diff --git a/README.md b/README.md index d68252f..648bc79 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ ### We plan to use our existing domain: csaba79coder.com +## Basic setup + +- See manual + ## Backend Dependencies - Java Development Kit - JDK 17 @@ -21,7 +25,6 @@ - MySQL - Spring Security - Log4j2 -- Validator ## Frontend Dependencies @@ -36,6 +39,7 @@ - JUnit 5 - Mockito - JUnit Jupiter +- H2 Database (in memory database) - Spring Boot Test - Spring Boot Starter Test - Spring Boot Test Autoconfigure @@ -59,11 +63,19 @@ ## Future plan +- Improve test coverage (unit tests, integration tests, end-to-end tests) - Implement login form and Spring Security - Create separate table for roles (and set a list of roles to the users) +- After roles are implemented, we plan to make a separate REST API & Thymeleaf for the user (now only admin implemented) - We plan to use our existing domain: csaba79coder.com - We plan to make registration with social media - We also plan to make a mobile app (and using google map's API there for the localization of the users) +- Creating javadoc for the project + +## Collaborations + +- GitHub +- Postman (sharing workspace) # Created by: diff --git a/doc/database/data/litter_snap_address.sql b/doc/database/data/litter_snap_address.sql new file mode 100644 index 0000000..a96ddc5 --- /dev/null +++ b/doc/database/data/litter_snap_address.sql @@ -0,0 +1,58 @@ +-- MySQL dump 10.13 Distrib 8.0.26, for Win64 (x86_64) +-- +-- Host: localhost Database: litter_snap +-- ------------------------------------------------------ +-- Server version 8.0.26 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `address` +-- + +DROP TABLE IF EXISTS `address`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `address` ( + `id` binary(16) NOT NULL, + `created_at` datetime NOT NULL, + `created_by` binary(16) DEFAULT NULL, + `updated_at` datetime NOT NULL, + `updated_by` binary(16) DEFAULT NULL, + `city` varchar(255) DEFAULT NULL, + `country` varchar(255) DEFAULT NULL, + `first_line` varchar(255) DEFAULT NULL, + `post_code` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `address` +-- + +LOCK TABLES `address` WRITE; +/*!40000 ALTER TABLE `address` DISABLE KEYS */; +INSERT INTO `address` VALUES (_binary '\07DEN[F\ͧO','2023-06-01 21:41:27',_binary 'gr\ܧH&:\vO\\','2023-06-01 21:41:27',_binary '\Հ\GՏX0 ','London','United Kingdom','London street 100','SE61PJ'),(_binary 'P$Z\(K\"Ƈ\','2023-05-31 22:12:57',_binary 'gr\ܧH&:\vO\\','2023-05-31 22:12:57',_binary '\Հ\GՏX0 ','Budakalasz','Hungary','Gerinc utca 2287/3','2011'); +/*!40000 ALTER TABLE `address` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2023-06-05 23:05:04 diff --git a/doc/database/data/litter_snap_litter.sql b/doc/database/data/litter_snap_litter.sql new file mode 100644 index 0000000..0fc90f9 --- /dev/null +++ b/doc/database/data/litter_snap_litter.sql @@ -0,0 +1,60 @@ +-- MySQL dump 10.13 Distrib 8.0.26, for Win64 (x86_64) +-- +-- Host: localhost Database: litter_snap +-- ------------------------------------------------------ +-- Server version 8.0.26 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `litter` +-- + +DROP TABLE IF EXISTS `litter`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `litter` ( + `id` binary(16) NOT NULL, + `created_at` datetime NOT NULL, + `created_by` binary(16) DEFAULT NULL, + `updated_at` datetime NOT NULL, + `updated_by` binary(16) DEFAULT NULL, + `description` varchar(255) NOT NULL, + `image` longblob NOT NULL, + `status` varchar(255) NOT NULL, + `address_id` binary(16) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FK8r1vohow4x5bw1jgq0m6c5ayb` (`address_id`), + CONSTRAINT `FK8r1vohow4x5bw1jgq0m6c5ayb` FOREIGN KEY (`address_id`) REFERENCES `address` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `litter` +-- + +LOCK TABLES `litter` WRITE; +/*!40000 ALTER TABLE `litter` DISABLE KEYS */; +INSERT INTO `litter` VALUES (_binary '\H3B7,i iAe','2023-06-01 21:41:27',_binary 'gr\ܧH&:\vO\\','2023-06-01 21:41:27',_binary '\Հ\GՏX0 ','first London\'s trash ',_binary 'x\\\?ӏY\VH\\VDF䒻\rϽ ٌ\\'I.\M\\\ۆܶǧ\~]d\?t\O\\:Ɏ\nD\\\\yw\B |J‚\!+\|cYxHx궗__D\E\\\Jǭ\.ƍc\騐 w\!%NH_=[XӀ\ ˲\>:\\I?ӸU\p\{f,\N\\\H\ 2EO Ռk\ sI:q\X0>#0W_r=+MSyP!&]PO,8(Xx\IIݣ\\ \m¬-\Z=cl(X(\\({WϾKJ\[z\\}\\F2rȚ\I;\\\)O㘔@i\r\$TP\ Ԟ\\\3\\vA쒹\\{cU9^\@h\0\[ 0\x\uO\mJ\\\P\\\\ɑ,r4R\\xNe4\ܽlD\n.}Ê\5I }Vu$sn\v\ʊ!Kr[\rG/šY\\鎎2\\hZ\E\0\s`|\\vB\\t\ܮ\Z,O>Ha\\Z\~7\OТ9A>e{#ι`Ǽ߈\\NOR5\\\\\X\0W\ZJTe&6\]\\'\f\"8UL\/-[.+]\P\br\\Jm\\M\~ל%\Z?}P\|Z\\\\\#\Ї\"O\\ \\kiwF=6[K?AƗ1x9䒧~V\\\"1Ƃ;:l\Qe\Z\u\h\r\\wc\\5\\s\\\f\\IeJDkO\\O!\1\\B–#}N\Y&i\IbUz\\\oQB`#{U\>żlJ\hVũm}i\\u\zqt\\4Ͱ;\\'uJ\2e-TpE\g]V9\"wѦ7\䥽*.\s\i0.̓ J}g\ܩZWyU\Um+3Q\Znwj*丙\ +?^ f:\8J Q2\U}\\Pަҏ\\ \|\q\\χ%z\\|KV\;0\P\WۧP\\PWE\j\*YQR\M\2\'\BE./Ok Yc\:bt \?*Ya\&.\6#c\G\1e,\\Z}\<\\n\N)$ \W\0`]q \0ks\+|f\&895վH\\Y~8=A\dN\|\\0! 7\ҰC.r\\:\0\[\\Z\rZJ\\0[(UuGFX\#axJ\g?\"8NveB\\McM\ҲXD΁\\"\{I; sv\Zw\դ\i\Ů\la\0\0; h\0\'O\\"\+o\h\\t\\󈄨\0&\:k@䣈\}mFGfqFE\JB`01w\D\zq\"[փu !N\m\z*\aVI\\D9\n\(JAe\\#\6=4\oP\'\\0?ϫ\\"|}u]&\' {A;(\\ef\\\'O\C\Fo\؛dme\\E12q&ٺPC>\eGńn\{j\\=t\C\=t\ES\%,\}z:zE\P[+M1w\c\\r\\\\'Ν\\ܩ\\7\\ǿp x=\\\O8\h]\\7ͥ\Zݖ6Vb&pc\0N֦,ܥ\n8\n.]T5 \@v\REz]_\\\V\ a7UH\uhF\n \5\&\\\ޟBRfM\'ؑ\0\*G\n9§fF3\uf\:\\I?ӸU\p\{f,\N\\\H\ 2EO Ռk\ sI:q\X0>#0W_r=+MSyP!&]PO,8(Xx\IIݣ\\ \m¬-\Z=cl(X(\\({WϾKJ\[z\\}\\F2rȚ\I;\\\)O㘔@i\r\$TP\ Ԟ\\\3\\vA쒹\\{cU9^\@h\0\[ 0\x\uO\mJ\\\P\\\\ɑ,r4R\\xNe4\ܽlD\n.}Ê\5I }Vu$sn\v\ʊ!Kr[\rG/šY\\鎎2\\hZ\E\0\s`|\\vB\\t\ܮ\Z,O>Ha\\Z\~7\OТ9A>e{#ι`Ǽ߈\\NOR5\\\\\X\0W\ZJTe&6\]\\'\f\"8UL\/-[.+]\P\br\\Jm\\M\~ל%\Z?}P\|Z\\\\\#\Ї\"O\\ \\kiwF=6[K?AƗ1x9䒧~V\\\"1Ƃ;:l\Qe\Z\u\h\r\\wc\\5\\s\\\f\\IeJDkO\\O!\1\\B–#}N\Y&i\IbUz\\\oQB`#{U\>żlJ\hVũm}i\\u\zqt\\4Ͱ;\\'uJ\2e-TpE\g]V9\"wѦ7\䥽*.\s\i0.̓ J}g\ܩZWyU\Um+3Q\Znwj*丙\ +?^ f:\8J Q2\U}\\Pަҏ\\ \|\q\\χ%z\\|KV\;0\P\WۧP\\PWE\j\*YQR\M\2\'\BE./Ok Yc\:bt \?*Ya\&.\6#c\G\1e,\\Z}\<\\n\N)$ \W\0`]q \0ks\+|f\&895վH\\Y~8=A\dN\|\\0! 7\ҰC.r\\:\0\[\\Z\rZJ\\0[(UuGFX\#axJ\g?\"8NveB\\McM\ҲXD΁\\"\{I; sv\Zw\դ\i\Ů\la\0\0; h\0\'O\\"\+o\h\\t\\󈄨\0&\:k@䣈\}mFGfqFE\JB`01w\D\zq\"[փu !N\m\z*\aVI\\D9\n\(JAe\\#\6=4\oP\'\\0?ϫ\\"|}u]&\' {A;(\\ef\\\'O\C\Fo\؛dme\\E12q&ٺPC>\eGńn\{j\\=t\C\=t\ES\%,\}z:zE\P[+M1w\c\\r\\\\'Ν\\ܩ\\7\\ǿp x=\\\O8\h]\\7ͥ\Zݖ6Vb&pc\0N֦,ܥ\n8\n.]T5 \@v\REz]_\\\V\ a7UH\uhF\n \5\&\\\ޟBRfM\'ؑ\0\*G\n9§fF3\uf\{^k}\ܒd0iJ(t\]\s\s\>\\o|\\"c \:vo\\\:^\\\u\nN\xpF\*\:\Uu\Z]ǫ\4W\it\\:^\\\u\nN\xpF\*\:\U\\' \0\0-`%!X ba \\\0\"`XL\*XI` \0 L \   \P\0\\0\ U(TfAH\%X@,, \0\\!ӋL`!ka Ă \B\\d-@\\\qm4\"q@d-\0k- r\>y\0 Dfb\q 稸H\spS\ t4BG\T\,n^h/p+t%[:[* T *AXI:ɭ\'Еd rm\0@e\\9QBҾԠ- \\\a\dV\r8YFH8\=%H\'NSdH!\\\\i@j\S -\g(EBW}Lk +`\0B\B wMBB\n\Ȱ!.\"k\n[\0-\ʞ\\n\/{z\\P.qj\Tj֝`T2[\8P\9\􍵎 \\v=-3Q\M,G\Ӂ_hăaQ4LT\r\n-c(˺Nu\*@!{0\\0$\\"1™p\t\A\D:\t\\r9Q_\`\\G\re\eƤU309Spb\\F#&K;%\\\"\0+\0 ,S\'\g\[\Z\\_6 \͕w\rஜ~rraGc\hr~b\\\'\搤\\%BȚTLʙ4\\B3\\\~#\VR\!`X[\ǟ\>.q%xRy\Dp\n@GRc]\Ut\w3\ /}k 37ܻ&\\Jh1\r^aij\\zc\GOc\|\yb\Q>yse|aƓh\\q\et/\B\A(0Vz*Sbpވr!J;\\W?-7,\0YHYH@Z@yK%1HZNٟE?+/+b7\\:r{ottY\\\)nJg\zҭs\{W7@\$LcC.\8[\D\s\ \')Nb;IJ9h\\m\s\\\ni\ťn\8B:\bt\(N\\oD>X^\\\ll(ɢ^X36j\ïۏ[gv6T\T5.,\\1\;\\\\\[\\O†O ~\\\{T\,=\w$\7䘬̲\'\\5ӈEaa!Y-P\\r\]\'elRֲ~{\v\\!^{\݄\ߠ\\Bw\]4+S37m]xtbr-9\\'Y@\\V~\Ʀ\8ѳm 53\\\3\\3b|愾\S\\p{O\mDj]Z\5ӞJֶH3KvˍqvP S>%xyZ\; s\V\. M4\n|G\VL\r\y`w\ndcki|!L`NF\KG\\tL\vx[b߅O_|\\7?\X2 KdAVd +\\T}.?N \n\Zm\3/9\%Ogp\y\\΃Vmٺ?a.Z\\'xT\\1\\\T\\\\xP5}Ә޳\&\\"hO\t\r\*ƼY &\O\]K\o|\r{\7_\Zn>+L@զEGD\\'[\uBw/\ȫo\z\q\9AO7\D\g]ov&&\9\ɶlQF017?3\'K\sjU \I\j^\n\\\vd-,:b~\\3\3\_ww\Q*DR:\\h,`A\X2\'\\/x\@oX}\\E7Nm\~<_\lΎ׶\5X\\\q9Y8ᤜ\ݝ\{f}sz[[\r\'\Oͧ>w\9\I}\=\5طS Y\5\ك\zgB;VUZ~77\\wO\vbBw) ʫ\rO\zBb\qoq{\nCK`!I:\YoffU\,zv\\:\D{\'V\[2\\`>^\\LmOT:dPw\\W?\ X\r\"XAYfl\FnJW\\̂BBm2W(u.\o=\w7uEsw\J\t\\[\koE}k \񥳳\\8~3 ]鏵7\|\n\\ o\\r`O\0T\g\o\-eC\\3^:4\R\$޹\Z\C?2\;1:|z\\%~s|{&+Í\/쾈@(9>]{߈w/\/\\n ð`\=9(YTY\\\q\\\\70Yy\\\\\\\@ZE\&g+ȆooD\%1M) \"t&\us?4\08\\DN\0\\\w׼A\\\Z\:bKd~e\]?#ޛ\Ƭ\iS\\ro\T\=h2:Joݴl\{~\ kQJF1spo.\;\Ҭ}O(n\\j4\d\T\N\~~\Wa}\O4!\W=Fr\\W禣{ \"%\&V^;ra3b߱E-?\\bQpjsWxc\B<_/pZh6gƖ?ztk\Z\\woap\bsaw8\Kh}O\=\\lh_@xv|ɊWDs85됼u7\ \ju+.7/\i?\a\mmLnJ@\5\\=o\0A=OVR\` 8<\Z-ǭ \'RM\\\\hw~\;RՓyz5g?S} N@>\\k~\'>[\'\\ \~ۙ*\\\\\'\\O\\/Ӿ(\\DC\\sk\N\\"l&\\\b \\_iذޓH/֗k\"K䬹\ܼ\GDo[g\\q \\bRSa]k\\rfsD\r[\'R\'UC{:a@ \[\y$Ǎ\\6\&*uș\\Kጱ\!\0\\!\0K\KSC(ͲNܴ\~i\\\\\\-\Зǟ~\{@Cvf{\tO]\}\X}\$j͎7\/Lȉ\ܭs\\Թ_|Mc~8?C1jLǽn\!\Q,\ VW\\pĘ9\S\ґ\\K\va3\fA?GO\vR\/[(sո\'8\TT$q\$\n\\/8髽\*)TlF6\\\\0,I%\0 ˼C!\Zk\\ B<\0F[k-\0AB*/\X\B|Y\rH/W* \r\\\' Ho@JelC,B\#\\YJX\\3.N\\aҭ]\\~zX\\\\\\,J7.lNT\\\\\A311̞Sxƴ\\r=|hb-|[%0\ fý}ٛqAlz{ȕAMa} \qK\ri\ؑۮrs!(U \~)-\\S\9ᥩjq\׿moDuFϚJ p8J\rTDuz~\Z\\\V3ءIc/+@xu}\l/7.\0ыV#!H!!TJJW*%HYk\Xk))%A\Z\!0\9b\\\o\^\\F.\\nd @x`8\\ZA\"vWp3\UIK˟\\{^S\\c\ ^v9wrG\'\7mC\A!o\4\Z\z^|1\Zj\gW\{~\\\}z_H)\\n\\|^Ū\\a&#]ojlg\G\3@\^Px5[Y\\lN\9ytw\n\\x\Δl^x\\\r\\r\\ t t/\.W|+=*I\\3\p\Z\\0\K\ٹ:5\0k;\;\ HFt!ЋY \H\ k\qG\!,3[b&\U\n;B\;x$k2WPI +\\\ \r2$0\0ðI 2=]H2\\snIMEC=\\se&DTz\gfq\\jKd<(c\\,\r\\\n}\\\l%NP( \\=\O\r*72\D KSTT?\\^3\\\TB\@Xt˰\0\\jg\VQQ%ٌB[\.&nx\\!4+\0\ViР2aSK5:о]Y\\0\&h\0,9\a.L %\ˣE*\"\"H\\nc\8\0#\?\"\\\b`@ A`Xcv 6\\\B\Q$u,ڙpzW\2\MDO_sPS;Ȁ ,(S,,X9\\L\^`|9Eĺ”]\'}HӢqg\\G0[\\\0&޲\IV\\S\GW|\>5Δ\\Z\ˊ\\WVtǎU&K%멈\G\h=z\ʱcJg\\COK\ \\\Da\r\\\-2>\\K%\P笞\'(\r\۫c\\&ϟxkO\*c\\/\; 6M7c\\^\6Rj*(\֧:S 6ՎS>79\\r/\a \0Y~y8# b\nQIb\\ idrff\ΈYq\ZRJGCPRHXÖ\\W0@ˈ򲒍,+sD\ow]@\\02r\\{6A_k3==u\\\\\\_\\㺮\;A\02aRBc\<\^_Τ\u,_cnt\-d\n. J/~A\\l8{\ɏ~lD\F}l̛\huv\h\'\\c\η\\\\ab ܅H \\G)t~\N`W\\{\dI\\V3hxQ33v\?`vtr\\n Z^1.t\f\l\G~?v\?X)7p=\\nok\<*؊\8M\zn)Bk\/SaP1\ms?Ʀa\Y_ Gq\\o÷\u\0\.>y@0R!ˣ\\\p5\\+ j;.ItĐ\0+X}7G\ ?oն7_8\\ӏX\r\P\h\r\K\/\\\z] . _U3 ՞ow?\\un!\$\'b½vQ\J\M\Q<(r\\[*\~\'n ~\9U\Z{[\\$8B #flǮ3H;S7U^s\\\'~֥ũj0Ψ\ZTA\I֞\nc0_9p`\\\\I\k\vt\aOUKpݔI\0\Z(gU$~PDR7\\\rl)K[^/\uV\d\\Q \eY\\_>Qb\V/dDTU\vT\Z\\ؼ$O\Z8f\~\_\]\Y*\n\A0a\lV-|7\0_F[/\Q\!\60P8\rf}\ P9>\ld\Ò]_\ߏw\\n\\٠Z\}\\YABh\\}ͦ+\\\\\\\\~Nӗ\v(A=VRhV\\,j \\ xӛ1g\mn\\ d\\\85ƶۑ4DhH \n:+/XTq[\(N>|S\\I: Ԗr,\Lr\+ gf\\9*h\n7\\ \K\tu\\\=eP=p\L^i4˪V_\\\c>]N|y\\xֻ-\D\8\;n\2\8\Z\\Zk1R!B0 \X+3\Y\\\q \\ڼ@~~\~\g~\\8\f\<\0\aQ7 \)F\ZΝ\iH\0f뒄\93 !\xȗ\\]7b=\\n\{~M,\ťv\1%\\\\1&\0AJw\\0\ݰV7Wjw{[X\k\[1\d#\\AoJm׬͹3\\\/p\8{\\O\\@\0$$ \hQRՒ\Ejc?jeo\\].\\-[1T\\c\\(E\/\K0D=\\0y+]S*\Jm\`fc_\:@\"\Ri&B~Ï{olm\r7}\"Q.Wn\y.k3Y)\U\QQ̅1$᪾\3k-\,\\n?ي,\s]293Ea(\\p=\W \߇aq8\۾@l%tv\j,IW\\r}+A\0P\0KkXV\"N@\׹\\\'+\F{\"ĂɒA\\r\'W0C&@ZC\$5-C^.$\Ʋ1H\"[<9?s,\R!\Zk\nY f-`!-\\0=`,Z6\\\w[o\\\S\\9>\'|~qm~zkn\\K\UW\Ai\\c!@ڪ JȺI\.l\?<\rg\5f2I\L\0 F@m\2!\p5Xљ \\0gK\n. DZ$*\\Z\e\?~+.\{pƮ\O5\Ar\cHB\ \Cgλ{v\#躗L@ Y( s\X455\h\\+ vb\/?Ē\\n`\2)x&\bilI\\ OAX=@9҅\HQ \0Py\>B\nȝ3}\\08͆JR^D f\X,勢\x\ݓ\ms\\\oN\{\\W\]\\\?b:ܾ\w:^\G2\\Zk\&1[\Pf\\nz\r\\\rmZ\2$Y\\`W\+\\\K\<3ZG\:eY\A \ (@\\tM\\Z^z\\n}\ 3j\\r\\\xp{csv+.~\'Y\\52UN\иNWc+\n\\9\tis w߇\\.{6T\Nj\B\"5\ހ\\"\aFTsy\7߅\B*feI\Z@\ZA\\G89\m\\릏SI0ҷdME@V\GCZH%VP\`Ap\n\\B0e!Їj\\0\yFD5\0 I;$\0V`)\06B\\'\I\'FE\?\O=}G\\\\.V\S\'Μ\\\o\nY\@\Z\z#|BW\ а_Y\S™\HB \Q.\Epa\i\+\\n@{o\c\\|;( h[dԾ{j\@cq\VWW\)-_ $&\,)8*1&1\;\\Er)1\Z\0P);}.ƶ/ħ;q)k_xk\h&ɼHx ЀMS\8.딜>n\\xk/þ(6څ#+\0r\Ii\ ’]e)rm\,ˤ$&9?I\"IPLֲdHb`\0$\<\\:kأHm]>l\[\\\\\j,wxlႉa ]Gf\k]~\\8/F_\~\~wN~\nc\y\lmZnh0U}\\َ\0\x@MeEj2_js\\\" 8,R0E\\%N\}D!Jg\P\\\H\ۘ {Ud6\ュ_jo|`x`i\ũ_.F#Bd%2̐)$3Q\r\\*\D=@p\1CD  \\5D\(\Bi&ä펶PX0x\|s d`M- !#R\Njo\\0DLL\'a\6\\\SϞY_\hnK\\x+\\\hmnBClig-J\0@D\<6>\c-\nmB?\N\+\HRË GW\\jk@$@E\\Dk \\ ULZD^ޯ B\\Z\k\rA cs\SrQ)HlEn8\=r\ \"2T䦀WNrqա/zaQ\a`\Z7{\lYv}k\O:\\\ \ H @\0&HJESTJNU,[-\\*~\\2-JH,AA$H\ LĄ\\\w\\'\\\A\`1%\\}\w\9\^[\iKQ\ >X\m<0rɬ(޲\\\0U\:\~\P$Lti!\s\>0\\\\\ y*\r\.8/\(Jp \ \\1\~᧋2tf].\bbK4+effbW?=g^\+ra/ۿh\\N}y\񫗶F\\\\>p-\r\\o\g\\_&‹\\\6 \\֬+~\\^n\$-mD)=\꒧IE!mDO\ZF;عQ\92 U\S\76/\r\j\`݅\\8NDGQ\N^\\ZE_e#\'P\Vl\",{k\| NPj\npe\\+\\.\]\\\"˜Ʌv6Σ7N\p\0%1@8R\ZzK\!0\n8UP\񥒰_\\\><\n3\^\"\}N0!\\?0pq\x\\(LBz;\>/~o$L\8D$\r@\ns\\H_\7\N|py\n\\\\ \\m~\\\1o\O}\NpW=>\$\f\]dw\\r<\Z_\\\0`0x\\6h\,\1\0:s𕴁\2;\\t0Pw\\r@\0D\ 7\d4w\\\=\?dw\7\}q\:\\YZv\@6\H\v\H&\ޮQ@Į]\\]\F|pG&v\*E鳃,O8\ШLb9N\\nd!v#:Oձm#Ĵ\4\"͹\R5Jυ&𖝂#xs0\\?\?fsI: *XZZ\VVVF#~Rc\ k&\"oqq\0f\r \΃\:P2p!<9ʉfMzND\,)\sP\P\\\\itA3\\)덿1\|!\Ў2EH\?\:,G\3B\\'9]BA$\a\}GH)\rnS\Z{&X\E\]\'\\ZkL)D\0ҰNuʃ<@\2\r\\ɕiVXAC\\\քXg2fv3QXX[\/-[\\\;>?\9c=\_\\^[^i\'`ab@J.m< S\\[y\_Ɠ&s[\ŷ \Q\S&\\WxkkҸ4ߦ\\\\hwNh4k\kK exV[\\ZU\r\\\B%iZe\0D`Y3\\'D\B PY0\x}\h\\x\#2\\\z\Iޗ\E[TU\'̚\#[~+ȭ3@`7¨PI2\d*oy[kArT\$Su\!&\0!-t\\Czv\\\\'@ *\`7\N±A*\қT$B#Cx\[_-vv{G\\\\>s\ϰ\\{7Xg\\+\\>!\r\Ҡ݊\\Z\@v8\w**\'\~e\\\\\܁[wz=\\\F\X[{\\\>K\\Ή \]zyn\\\(|e\\[\Ԧr\` p~\HA\C8i-i!9 \Z\פS\n&E\\\\\nO<#\k/|vqĬ\gH\<&󏜧q0\\_x\4 R@\)X\JI\ {+뭹\o\\ı7&ŵ`u\籤\r\\I#\\g-.r\0%\Q\U0\\P<\($[Yf\ L\BBg o8`\l\$\ng[VZk\?\\/=\~\o5u\l\\jey\Vg `MLB \ bkvcSMIsPbUc1\v\6Œdu}\1T\\Dg.:|d(\~\h\\l( A\ЁC\$O\E\d\fo\\"EB\bF*`ؓ\Ab(#w\Iɪ\Zs\\\0_a\sQ}\\\\EP~<\ZA8\\\z_\>X\KO?8H)Nk\\x \npV)J_\\\\qɿr\w\\\8\W\QĐ9,֝\0 ]\pq6\)\\w_\\\lP \neT(\y\\V^\Z\\_T~~̱\Z;\\V\hƽm\Uޘ_Xn;BFY\\\P>񺖤kk8⸕kZi6\m\\GѠ\i\z3\\\v1q,\muu[ox\(\k#\R\f\U\nQ`\\\Ĭ\`@\iց\ B\"\=\ը\\Ue_\\s@ބ@_\HEBkZ 8kka;\\\\ؿHq\\14\@\I&\0)Sy\\\y0o9z\]ok\AU\r0xxg\O8\\|%S\y5[5[o!,ʇ\0`01B\8\\\\B홍\\n\z\!\41\ro\"r:\4\0>@V\S\0\0 \0IDAT;`l\\\:Q\ \ہ)v\I\}\\O\\x4\e\q3מ\>tgW\/nmm\\\2\9\z=)\d\\f)\\oϷ;LǪ\\6jֽ,\z\GAZJi|E$\2\7\\$\\\²s7Fxy\1z\^I{B\ \:D\)f\\\͘l))`J/PAY\:8ս\\"-*8\\1pyL\J\8)*Wa\:Ą2坲l\G_\z\_\&Ϩe:&\0\\"t\Tֈ\4<1즬˵E\(w\Xw? ߸|\\ݧ{.r\T9y\\!] $\\8\B\㬨j~zI\o=\p1;E{I\w>˟xcs]\P$Z\?w \/&osd\\x,lc\\\y\]+\nU\\04/\\d\\\AdE^׍6Ve\i]Jvlb\\ EFN\K0\ǿ~\o^:s\n\8\\Q\;G!:\r\\\2\Ó7\Z\ \\X+\\n4UQJZ c`-\\J\`[:`G\r8\I\Ǟ\nKڐ5<􅧳\Sp\=\C?\ʣm\\\\燏:&a\y+PA\a#F\UW]\dsIM4(\FMul 3hl.\%k\s%\2pW\\ŤZO\\r\J[Q\0x\H\`X~x\ɷ\=hm!~dv\23\Fsi=!$ÅUO{=PB≯}m$] \h\\3\\\yd\J\aic,\2!\0\j^\\d\"lX\+WZ\\QVO\\p\vih\\Sq1c7\Y\\29%.V{R\)wM\\(Ѐr\0Fu~\ K\r\ \\:˫_s\=ؿ\\'\U\Z)\`&$-y\#tW%\'k\rn-\aP {8\nL9ˉEF 祇\5s~m^N걤v8DZ[c⬜\\rg\2\\}\\Z>\ߘ,\%ՈA;Y\L\r,´\}\\+I \\돝Պ|=\$b++\KrJS\,\)i)pN{[Y\ \P@\ZcE {wЁͭ\\\8/\g\‰\&x\]ʨ\\@ AN D\0We\\qʒ\YF;\"%Puj>\+\C݆\\R?\0\\F\n\! ZOI/\)x1\CރC\g{IF\UE\a[\;\Vw~A\8/\nq&I(u9f+ro0 @\\\\\\U\H,f\'\_.h(5c/\\y 0!T\&F\΅\)g\syM{\\96FQ\0$\8@ux5d<:W:v#WKx\H\\4\>}\\\w\Eo{7\?\&\\.0LK\'G\\ُމ:t3H;ǑK\86!yW\I-\ z!륮 >#XT@\n\0z L\\\\\|\ms@\\0E^Q\n\06z\*AUz(\\(w7ۮ(\\\n\;U7/\-\+0d}U\SEUfuY䙒qI*HLQ&d\ͤ4\ٳ6Ё-\0+P)ʤ\m\FZ6\Zu%xE&0\s\t\p¹0P\5\P\kaH@x\nc $*t\v\'}PȴJvT˶ò~2^z\^ܻG\vё͇(/|\\~Ϟ\׷\\ \g\\?\[4נ$\ *\9Sd4udI\.r\ -%Uĥf(Z\0^hOҸJ,\ۣ}\+^\4O<|\ZC`Ct] *TBk-d0ge9N\d@L\\Lx\~\Zq_\g\Q$0qh&k= z\ZRJ4\\k]U\l\\yh)E`\b\ϙ\IBΏ\ccK!i\\F8v\XeaZ\:\\#\\@˰Α#H \'l\\\\&8f1W^\ѺS\_\pG\nn\ \;>\߻\\'ο\'>t41[K)\ٸT)\ZA\\x\Z5\0*\\@\rL\\-\S.\\|\\҄6\z\ l|\\+\\#\[뗂%\n\#\26m\1\\=\fy\\\O/mꭥPַw\\\wZ xk\\dC\\_?߸)vi\hw/2y\{\;66wW6w\\\O?\.--:,q\\8\gYv\{\X\^eYwa6/\nGطe\\\Ap\IxJ2\'3,i㣜 N^UI &l\NML ڰ\nP(5j\֔h\b\\\Έt?ЀfX!\"DŽ# \ Z\ݴd\f\\s35\W~\\\\\CM\ՎqUe$\0Z\q4\\DT+\l d3\:\\E\0l#0&_*#\\V}bz˛_\vF\߼\9sh~\! a\>)Iv/0\\\\\/N\ds\}\:c\0 $xq\\ٳGW\l\jԍ-\Y\8 ˫o}\Sn\ŕ#\N\uR\Ӌ\4eQ\\\ŹIQ\\\iXotvv@n\(^[_\'VERN[\\أ\f\- U9l? 7_\_ؾti7o\nH\ظ:\]Tz`Nr\lp\r\dXb.\0\Z$|REX\VI\\\\t|◞\\\\\\\\P\(C\Au\fp ؝8KE/\@D(4 (\n\\j$\\9+Yͩ%Yu/?~\|rPku^oj~\"\Zs\.m\Zx~|\`i!\\#ƣ~k\Y\ƙg7z\g>N{\؉\\c;k;o\\\o\\ë\\Xq\Z)m0fv7LcM\7\7w{\Z\igr;\nǻ\QG\\\4\k(Rl \[;v\ع9^.p#a\\ UEa\\(h2\\d\\zAw77\rQqH9k\\\ZBYC\s\rڣ`4\`J4b\"4BYǽ\\\\tW\\\4VO5#m\'\\\zŕ+U/(\g8\8xr\\\\rEm\\\\%:I\Y\F\p#VVk\Pb޼:}\\\a5q(UɄ\a\n\M\Ѩ~D\'y>Lu\LO\"\r\\o\o\Zcֱ\n׷{AX\n9\'[[[\\8g\/;\\}\\3?L\jͪ\rzYi\s\W6\\eIpʤv\8Y~֜\60Q\\G(^y\]\'U\"\\N[{9{\#\{VkIȺ\bL\\_\HqxT^z\L\\97^?7w\ v\nMmB/# H\\90#\Z~P\\⥿\\J\G??w\8F\rsmr\>c D;#\\! ;$lTGIX&A8ɓa\qncrt\y;\60Lʙ\V-n\\'T5Hi\e\\\VFFFk:O\\_~9uU\[\J\8VN93\mLʚJ\h<1l}\|ӍB\n E7\1_t\\\+ݧɆu>\nQ\Rؽ\\h\rGQi\O?\\\\s}gN\\GU\(2\o] \\ry\i\Zf\\\4\w$\P_Gu B=d\0g5P0U=4\\\\ݘ\x5\ex˒u6R8C\ ̴܅cbL\n\"D\'5\U\\@\J\Wc\PGo}[gѫp\Q12q\\\\iؖ*\\QJsǞ$k*NE2Dԩ,\\\E\NU5\E6IdVK R D\0\\\\!75M,uW\8f7\Ij  bO2\?\c鼒RI\{jZ)Ws\8+\\\\\Rw~\\_4k7eU `\\\\66־ͧ>\?T\E\\P \'\\\\}^F\ݖv˞ڴNx8\n{JN2\\"4X8\nz`\\\D\\ivj\57M\8 .LR @\Z4\2dL3\H\0 p\\X1s \\x#1jY\+\+\\'8 \@\3gq\.f\rL\n@P\$\r0\\@\$G {h9z7\@S_rJ\x\0\0c(ȀdɀJ[@B:[ yt\O=T\\\TFw~ia\'\K[KX_\>yPkg[ ~F\Z\\\Zi\ ~\'%\\B#bI \\\r\8ek\Bo~k 0{\Z7\g1UJ2\5kʳ +p\\'E98\\"@8=\\ \h\b\`Wt!S~\\5%\Lr\\Jx$v\7\M!X\r3n|\\i.8E `F\\\5/P^ƚ!),A\\\\Q\̰@Y\@8t\nPnl\$\\\\\G?uϙ3OnZ(\r\f|\ҥ8\dqq1˲흾dt;\{\j]!<;\\z\yoE\os뭍\\r\lNnR\\ 0\\T\Iy\[\\\ɢ\>5< rl8\Zpc7B\'.\4\;\\in\l\'z0ء)+\Vu]k\\\\\\{\cPJEQk p}\TK\xXՂ эf?:ݝ\;)\\\\a\yxzU\;\6\rn<\KZ̄@df=W\_ΔF\\f|\^\\"\\\rΪ fc,f\T-\7F#\Xv5!eit-j\{\\\C\Yβ\s\\Ge\za\띹V\!{\\g\7d\hp^\Eq\]\(\n Q\2U\\nfSj퓼\\߂$\k?\\k2_`\c\\\\n>I\n\k_\=[E\j)P\{\&eL4\\- FJ, ,\y\\9\'\n;\(y\\\b\\e\ϲW\n\\\\\G\\\\T*\=4\\[75ŷ\9\\\/\\&\Z9F:\\\\\=ſʵ\b\\%W𥭑\"@Q\V\ۼl\Z2EI\0\ \vF\hi0>{\\J\n4\^ݟٟu\\\$A$ٙ?|Oş\\\\6q\{!\홦6\\V]\b\z\ܴW\f[\MW2{x\\/\~͞0.8JՅN8s¹\|*[d3\L\nu\'K\\\e6\v\l6$P\n\\hc\\\Οȣ_{F\\o\\@\rn\Lܒ|ѭ\5oO\\\^\J>\Fn6;\0!\.\0a\\|ܷU\"!W쵳ޖp“\c6I}\#?e&\ZkA?\nT-N>]y\[,ՏCS:3ya\t[\l^\A=H~U\5W/S\\UـA\~rB 5\7v#\0J)iu00\9\\ݞkMY\:\,97ͼԊw.Njق% \4\r`\Ww]\FreA\\\e˯vͳ\1t\4؋\32N\ET\.vp\;8cәW\4eYe +?\sUYlmn5j\ϟK\ʔg@ C>\\\/wқzR/c|\|g\\Z\",7\L_\"f\' D1\s9ƕvRΒna\[k4\:dRRJI+\\<\5Gi-y<|\\\k4\=\6\F53}{%O[\jo\,f zI\\<\ 6e뒅\"\ngcLUU\"BLAű20\Z\\\zcaf٨٬o\G>\)N<\,k\k@U.h)\K雮\L\\\B0\[7^]\ގ\/&\0\0 \0IDATMe1%Ӈ\ .\U:&&\\Z]Cオ<ϧqΕU\n0!̣l0\0h4\'Irf\ѣoxDHӺ\5\i\`\nlj rf}M\\{\\/\&\`\r\֍WW\\tô\9\!\0\'\U0+\teE\\{\]eMsNȀEiW\h\qSMM\\0Iw:@;D\O 0͍\u\5\\ʷ]oSnh_\\\\y^yuװ\ts\G$\V5WL*%IF\nc]ETt^k\Y\ \|k{w}ckyy9 \\P\ZK[#\ѓw>#i%\Z\.ڃ\w\\0ڛ\qK]1j\\T^v\\K\Q\o\\\ />GD\\ \Jg\\0\\0\\\e\A,\R@Y6̳R8\=a4r\I\\uJ)\'e͕UXf\\9\B(!OL׾\{0vr-xPyXmXxY^;#<<\ka6\lwoy_\ZWUłX\9!.>t^7ZU\0\$ ð̋A_U~\\A&R*\FY=)r0Q\~\"uA\r#,Y\\ n\ZfAa2\97}*ufe^La\'K\^K[\AH%m\\\\j\3\\gڅᙢ(Z\\\\\?\j4 \za6\N\JeeY&\3M\x=ݵ\\\|F3\ZOi!\~\\\ZCWZՊB\E\\no8\Zea\rTШ\\PU~罡\c=fZsB\\Ԟf\"\\\\\z\/_L&sssZ\\\i^|֨Ғ,\tâY\\E{ns/[|\'_OIR\($I\Z\O---\꣏:uwvv\\\6\\7G\G,,HL25YN|F3[6ιd47\rAp\PG+rVJA1L#! ^L\<ϯCD\\\,,\C#:\4m[ ̔д\z@SmZ\\BI\\$Is\g\ά`,&Pם|k5\-\32 ؕLƟ\\\\\\FLJĚ\{E< \\\K~?r\\wiw:.\\\\w\Z\_^^>w\{~\\`\\^\\\bF3\9GDRJ)\z4\Ze#\\kWZK\ \f~\ءSO:?@ c f\[ϭn}\\9\y_\0kF\\'&EjR#ԋ\nL p.O>\\\Ը\ :@\g3\\\\"\\X 4vXKDkv\\}v\w\?4tk[,Ge\\˧x0\hHkV;KH B f\,!Q\.Q)\0b\p\M7\\wiϞ: \9H%\n(AJ-C\0\ V6\zG\\;o\\\\h\'< y=\n\".\\)V{I\:\1630ȋW\pˁK.5P#\ۍf(K]Tsw\\gv\\MENx35\F\n/<\\=\(d\$ Ԗ\\I)#\K\\7Mo\;m\\nvE/.#\ǖV\/n\\مOX}\O~r\k!k\Q,8DO)!DZi5\[g\:g\A&1\\\uf#@q5#\{\\7X\LwggOήDZg\ \\*E\0\\\䬊\<\x\$ 4Vں\\F_kF\"M\y9j5ڗ\\rɝw\y\7F\\\\\\fsj\\YR\\I`\,>}\U7?FD!k$a\k&0\N65\\d5ޥ\@, \\0d\0\G}Vy\E\Y\\/\]\e\h&\q\HeY(ޟkB (AD/8/e:\nՓ\nc\\8\'Q(\(I\J[4JjF\0\7\Ta`+t\\ ?Oۛ|\\ss\Z-\"\$C.R@B!\\\\xg\j\C\*i+]\\\\n޳0v\R>f7dme\~G\\\\{`ϟК֛V;Pz\\{\6\ʈ@_X<\j\ZN^\\A vHmQJ\Z\'up*N\9\"Pc`<e\4\nm-<\o^qB\H)\\#ރ\M\{\rpf\w\\@6s\u3H\F%e\c\g97,њ\"K\Fv\\:W\$n\ұ\:\\,\0Mϩv\:\z\*-*(c%#\0\H\\g{\I9s:\[^{\\V}ݽ{;\0 w\2:IBzx;@\\\\Zl\mO\x\\hR9Tǂ \0\0\"B1_]N~᫏6ۭ(%k\h8\)\\0\z\\:o0Ph]Zk gc1a<> ń\V\I\MJ%0N\0cˢ4q\"{\\\Q\[+#s\(O\S\v\\BѤSDjU\H\I#\r\w\L*\\~B\3ƾ\l\XMt}\'\\O& \\\\0!P?U\H#\nk\\ kRT\LFL0J\[\0p@$/ea](E@9UE}M\Z{ٜ:\h\\)CL(@ \"\*&Aת\=z\6O-.4s\m\\rّ6 zc2\J,ɵyݛ\ ^hiH\\E\ZeI)\x\'}\\\~\\\k\s/sUg ӿ\?\{\\.oM\(kSy\h\l6\`(:gS]3T\zO$4\"q14\Z\\e\7\*Lb 4\W\\\\ /\`F0\;\ \y\0\0MTYieaެZ\\vq\T!p\\' \ڌ÷q_0J\01/\n\{\c\\N\?خ\d\5Zt+!ªr\E{PH1x,˜PːPB!\W\Vkm„B \'\Z\p\{t\\ޞS\\TȠo\n]\5 \ݶ}\=u\굹\~=f\S\ހ(9c2R\x&\'zKO<\jox˛\Z\At( \ S)ey\mW,\0`˘1&\Z\/\\.Dt9O-c8G%\"j͵tZ7GOw^[/-He\֧\ʀ3Fzd! ^)\\fݼ7s}\"5^@\nFü0\O\锖f*E&U\#I\#%P\0\KJF\'˰\x\\~ۿ\\\\\7#cLUU \e)oWbaT%\"N9)%\Oܒ\L\49\Tc\jFI\0\]`D(g\YSzG,@\0XOG\h$yUE4 2B_U¹s \0\ Ȼn\n*.[k\#P +ff\o\\\\iMד7ZɸR!c\"q2JJ\x\\u7\{\\|U\=wk:|\A\؄\r7!vRUf\d )v2p/\omO_qd\\EA•6z݅3O\F\TT\+Uو\\2O#۶\*Ê<\\X\Z\[\D7</[\lN\&\oW\Ah\Y\\"BY1:zɫ{\m\HR:y\A\0\\Ͻ\\\۲\X}\\SΜ]\x\䉿\\r\5\y\{\\Vʲl}G\6sd!ʈ\e\L\WJ\r\\n !I=mG=iKoE{\\_y\/[%q\c\X\Pz(!YNm\T$ǖ\WI\ܨE\0NIҰz\`nSΛj(!SʇsYP%ct{ߡ\'~\Ͼ\q\fq\"\#p\A,x\;\\ \pi\r\[{gNpv\\i\֔Y8}\\\\$QQ\%C\\QCAsP#RS\^\\qN\P1 \\\Ͽ[^{\/\'\\\U\D\x0\\\D\Y\q>\\4\Z4_\[\\R3L~mz\_j\'\\\}\\'z\0SDM;F\r\Ҕ Cl.\\\\ұc\n\\\!c\yL\\}\>\ {^t\WϞ\\n\"B\9w\"I=7ָ\\6U\à]\rGg\\G>}\M?C\\ҋlT\pgUp\\,\\%1x\`=x\۳o\t{\Y>ЃIa\\E9\IȊ^8\ժ*\\2\\\2n\l\'(i\ \]vT pDb4\d\[?c\6s B\[ \0\\5W\\¼\\0 \\<\\\Z\\Jo}$\nu1x\vX>i\\\\vx\=mʫ>]O\\\\\_y\\\Dgi0[R[l_\\\O\\SWzgK\\\-֍qN(\y65\,l\~\G/\\\}4kS=\Gggf1AG\Xa-J\\\f,1v=\W7֗;\{3\{$Pg\bڳ|}cy~\z\r\o1^q0\0\0@y\0G\S% \u\Y@Gi\ZE1sb\RJ\\d\\eA\\Wtjy\\[\\\\\\Q\xe\\`A`\E:ty\n\\\ZnUw^\Ï<\\\)\nɖQɴOx\#*k \`}}\ԥݲe\=\kUYe^\"زuT}*ϋ(sUV^o6Ƌǫ,\>}=\lkNun\֋<\Wv\\\PgOt\-\\\s\0\L\)L\\_.}\qojk\f79\2D@BRBU\\'v\V[g(\uJ6R*0B^\\Z(U匮 3v\|\\\g\z[ΓO>}G_4\\@s\n\jǂ @{Ӎ\\vp\\4\'\y7G, jF\(\n,nOiO8 z\U=(;iJHŜ\;^r\\©SkݍJKlF\\VY1\"\\~Ң\}7[:/d\=G/=p=3EMO\㤑mBL`[uux%\'ڄG@RJ΄\Ȓ{pD[}p@<\\D*\ Z (BU\\F\p:ʇ\L\]\۵c\\]\j\\\\Am쟯Y\\5\"4\(qK\\\r\\\=FfIg\r6\ڙ3\~^\K+K\\J\Z+LPVU\eݪ\7\WB5-뵯ٻ}.\4+5\tR % Fezfee\y,# \\Z\"e\B\ԾNv\\\;i\K\\00\0UU\-\:JQV\w\Zo\v\\=7]\\o:swhJ\ru\r8q\[_\\aף)\0m\\M o_{贶O- q\X6ϵֆRpZU,Y\\n\\몆l\)+ԢƐ6ΦP T m\\I=\\\;.!jPS[f@@i@\\_\0\\G@<n\ZCR&\lrgxʩ1\;T\x R)\ Hj*T9Na-<Ўv\ٹ\\:\\Fǧn=}I\꽏?/wW^{ӭ\,!.\h$q\j\ZA\rA\\C\HevnWwcvfZ 5|\gvRi#\@\8<@~\h82` T\J@Ԉ\ajaK%gff\0\T\\\)z+gGo|kԃn\9\"\[瑃S%\\gw\6‍顣W&\\'|\ÙFmzfyue\A93KhͺS!< E\\yt\\\\Љ0eN9u\\ $\*+chX\\]\\et`,\R\hcF=>x\u, c\\F[ 4q\\\\ʫF\n(i׊\:[\p߹\\\ynř#\`\\\\\;z*o/ҢXL `\FYK\qθ\\K\'أ\ٙ\pH2U\\o%g\:\̯^\\1\\\\\3\\^8!\N_q\\\a\'ICkvrmyqcq=\kGS\RZ\Z\\a1\r)\P\"!\S\\l\r\>\h\\L4NB)\\nX` 0\r!Գ\'\\k^թM\و\\\Z\01ԛb\΋~\W~=?VFi٨q\\'Vs籯]0:wT&;G)h &EGE\0$7.3\X@\\<\O9|Ț\rD\5GG73\\2;7\EZhv\ݲw{k\\q[v\\ҕ\?\s4E9\nF4*\YhG!x[\C\\rt\\FV\JR\0 CxG\1N\i\"YX*,\4\n%p:\\T01]Yzvq\\\h\\X\\\pAiڄv<\\\\\-oӅ)k\Uqa_\3\\>7{36b|\ӟxۛ\\\b ҅\r#@n~e \9\Z=gWCӭv/?2\\a\y:X][o\"P\\\aS,\\\\ \h.M6FS\\B\Z-\\\\\\`mW*1%5UdÍ2\!-zŌ8VIø#.\ؐG\X\Xcs(6,gP+\E7k\됅P\0ϾEo\'\\d~Zg\\tgVsRy^Z㋋\\\\\\\:zQ.uU֨\SVVf9\z\oxݑ\/\m\CVZm\"\"1y\*%$\>\s7\z\?7\#\radQP1\\Ç\\r\_yQ+a\lgssN{v6\lTxp4RQZ(T.#c\`0S\'Dʣ$\Y\:kEk3&\+%\h\\0W}]\m@\Hbt\Zo\0\|\\s\\\6\"4P\\x$cƣX|\U+\\_#\\H\TNu̳8Nlo\x\\\\\s$\\tTXM9\rrj\ZA-~\[n^9c=/\ FuM_w~8\n\赯{T?t\e\ycǶzK)O}8,ă`cf$ 2\ǃ!cRFE\Pkmˢ\"{Z[\P\D\q!ޢw\;\:Nl82JU\Z\:.\?\ @\0\\Z[{ҭgj\ոӌ\U\?\Z\S\Ô:z~\eW\Z\/> X\yGF\Ji2)SJݮ,u\8 Yr~\|}}I_A+\\׶m\\ڌ\_~\\kq}\\\\\\\H\q:N\A\Z\ k>faieʨNi:0\MDeerT[f\ԩ5\51\yGcp3,@ B9m]\\؄xA,`422[+@4N\\&q8x\(\\"+J\)PreGgܱen[R)e=eд\\\'K\#\01r~6\e\\\=\!LTҝ\ؠYK7\\S-\׽\\#򗾰:\\1!\;/ٸ!,\D\R1\DPi]W\G!Fy\Zl\r\`\\yT \mK\\M\:\ZC&\nLB\\\،\b i\Z:\;\K ,Xs~\s`,V6\ZMM\hk\={=\LGȢd^Y5ՙ\\^姟ݳʩZG\gw\\z\\Wm\̅q3+\8\J9N8U8Μ_\\B\AUT<AI5\Z\0 8AX^Z\\'\\o}\ǎ:4u \0P\Dg/:@3OJ qI !\h݈z\} Ga\h9\ZX*4 dE\4%Hp֜jʝXX\u\E)L\\\p4JmP\Ls)\)\v#\Q \RFF6EmF\3%\'\0 \(33]7cnԱ2\r jKÔ֚\\q\ԙ_|՜*KRi+\ttai\/~\j\m\!toj {5 1\ LLUU\0++k\{\/\CFqN\7~\}??\ja\7\/{\Ŵ\\\\kJɴ2\\HFӝtre0ovD\i/Ҡ\\n\"P+^3\\\U\\=\\6k;6`;<{|=a\ )N:hZ\\\ a\r /,\HSC \ \4Ւq\0e\\ZV[\\\"\\\UcqUQI\rY{\~\r\\\\F\RTC(\\\ȢƐ\1\N\\SH6Y[\;]F0\z\??عs绿\~\]w\W[\c\'^ȁ[\uĶ\_\=ǎj\\٩d*DTشJYԦl_Z~~auJ)cs\0!0\6\0f;v#l\/f\/\S\'N-//\"m쀝? %x*e\Q\(F\l$q~³kR\Dr}ӳ\1S\\1bBi\Ź,+[\@AxQɬIiz\8K\\*HҚE>o} ~5r\qU\PpNЇ:\\7\?\Z\\.}n\\\}\\\K\۱c\ƨ\i6ƃ\\O%\v\\\7\\L- F%Y\\_=:覲;6t\_g\'\ Y\pGQRo[J\\'6\#O<6H\"&\\&e\\*\pj\b!\ʲzPF\3A\\;q\@\\\\\?\\\>p͵\:pf \\;\t_>q򅹹)Q\\\b7XXZY\\|\ƫgzݍ/??1we[\Zuj2V\\0H\\rG k\G{v\\\"Q/\]ͨ\e\fD\H\D(\ =VpJE$\e\+\:G 9z/oc#H$|X,w 6w\\GPcEw\` ͪ2v\\eDp\\0 xw^ z<(崑gϪN<\\ܿ4M)gG/<({\\g\\'\\>Mm\(3\_~/\\z\\ioupr\lw\\Gp\ko\r\s\ʫy\\m\ؗ]_Ow\U:0\ _\|\ǟ:ѥ \\+\IZ\\8yh\Pj\!ʳ#C\3 PJ8\FT#h0\\Xaj\\k  h\v\qƢ*S m\\\\Q\\Pk7\v\\G\\\>1VwZ\\ޠ0\`y\")|x\YZ\œ4#Q#\0\'t%\\`eudܚ\\;EE\ +GRdHG\D\r\q!@\"8NG\\\ZtO6:\\0\8[ oL,Me\\\u\\\??w\3==\n:\4 B4sg 9<\WF\Z\\`\؄O\N\N]\AkO眮,@:ο\'\\fR̋\<ĉQ\9U\RI,D#\\\y\ܑ.\uw8t\hdU:\\(\\Ç>\G^띮j6h,*T󭉈\BKB@ޢsUf#BSBN\ydCxQ\0Ty\5:OQG3>EV*eTs%P׿Z\7,MO\\\\rƴqV;\3\5JUU\l%\ݻ\W\\\np!\q0˗ݟɧMW\\"ZuJI\:g\ٳg\r\B\cx}c)\qpk DPL$\'\WT\\\^wKt˧\\԰׭\hq\,\\nY\\ PPxz\@\\ru@xŨ\\")%%r@\;p\\\0Qʭ)xə7F:\ZJN3${o.FanK\\_ş\__v\\\l%Y>\$CcLYιzL\(qƶӧ_RjH(rRIku\\1k\q0,<~Ӭ\{M$/\8M\: #\n`\0\n%ӄ~{\\\{\FY\cϿpϧ\\ZwM\]\۾}*\Jy\wa\8Q $&\J/}wff]E\\'[t\F\q0@d\;!偠ʽ\#P)k\n\"\"^2JJ4:\GHG\V9%D\Vj4t\o44\R\B8q\Íz- \YkKϟ8o\5@\\\\~w~\\YQjm aN)%\^O€>s\\\\0B\Uٹ\"ڇ߀or\<\ǎ \5\;_w\r\o\Z[\\\N))\i\1\\籸Wv\R\59 I)\\p8\Z3\(Zk)%!dnn\\p\_Y\@P)2Q\+\[\\\\97sT6Y_\~ɵNꪫ\Z\\>Wv\f7F\\\`a\\\^R\+]sVk1\\˯\5,<\\\\ޛYZUwk\w\|N\U=\\\ \<A\\\rĠA)Q\h>1\ _7d\D\8\\s\n\\0s\\\\w{\?V\\j@#<\\\ϩ\3T[k\\\r\\r浼\\\6\Z \0RHX\]  N(8\*$ \n\0n50OcHH\\[֞s\O\\L;ITYtSӇ2?T,Ny^\O}\9 [D_-> ܼ|\\\exЇ$I(elی1{\02K\0e2\Z\c=\|o`` \Q\P4$W^\ӝ\\\O_u\\~뛶q\\q\6\DBF\\ޛv<܁}dW%7̃8cq\"pPj\qJϹⓦ\\7;5b\0\0 \0IDATj09c>3s\\\g(O)\0T\DZHgfY\NAPJ@,IJ9\B\\*D\0KJ\z\DQT\s,\\$MS\[neK\n}\dܢ\n*\\\(ha\75B8\0\0(,i\Mc\yUU S#PT\0V\gJ\j5cB\0Z-\ \7\\s1\Z~\\\?l|lht|8Mnϧ~\\<\\\\~{\ĺ\\\\g\Iq6+S*I4CCY\\\h润\W\nI\0N8< tan(_JGœo\0N(We\N)TI~40 Z\#c,\Z\ \k\0fc),~\1a\\3^rWs\z$I\0 \,˄DF\\Z!h4\ZJ%B JDvF5L%\}OE!!\b\oy\ \R/Ҩ\T\)TA\w\m\rpro$ܩ\\~ޕ\ŋ?\\js D&\w=K8>kdϡC5iE\4\?@H)p.$A(\cJn0\0\0A9g\0%JM.ߗ\d\ږ\g\\[lW(_,DJRl\9nga*\\0EQ4M2ƄA$ID(OE\x\\0\m\\ri8i\y\k^q\U f/^]a]qj3Sf\&\j\Z[\/-CJ\9sF\{\Jy\w;Y\f\4,+\'wO\빙\NjW\#R\n\Qҟb&8BApeN\.#8\RY\1%,iJ ML;++DJ]\k(@$&,\=b\\|\?Qo\\𡥶\Y6c q\" F$zL&*n|\J( wDm˲(\"\3Sk׮j힟X3L)UU^*dG\v\óSG%)\ۘ]\\␙\tfU޺a\\\\\ \0pT\ QPA\B @qi9@()\й\ea\RTݮ^x\y 0tYt,1\\'[/,\,\r\\\\n\\q˲$I$I\4\7\r9+t\q\9\\H=P(Lg\\E6\$LSI40U\\\{\\'5ոC\0rKZmffJ7\J>g\\\nQ\\ \\z9;,\\V/hɡ\/d)}\\\g\\f\f\FI\oC\:Ȓ\ \\3\(|\\\r9{\dɠ\=ͯ\N\M4\Cc ~1\h4E\0}bhY\\\H\\0 \rP\0\0!R\\*2\\\ړD\ԪRPdròT5Y$I \(\w\\-WL&=\\\-r\!\m+|\\>\\.]\4\B\n\,c\^!\ .\0(\"IUU\\ٳp\\\4\\s\rM`V¿4sh\\sB}?`V1,yW\\0\0w:|\ןu\Y\\.͢\}\qB\zj\\\Zq\p\oڲ]\\(M\%E\u\4\ΘcLs\q\|[@\\ \\\8\\d|>cahQ\0\Ǿ\O!D*\0…H,LXV\o~\ 8%u?[ \ \\_pc\\\h)\~Z$|,٢\k6FssL[@eT\l\"\\؄fZ\n&\$à\\v^\FD\0(\#cC\˦\r\\IJ]%\uwvK72$\8\plu_\ )>)\$\\\\((j4\Z\\.\߁\\\n+t\s>y}C\\\rDO>Z9r\0ca?@u\Z\$\Z Nz\1~J$IB,\nϗ+\0\s}U3$\ve1붝\no\{ߟK ˸S*(x\.%\0\0t\ lF~K`HOx\do߾~xDnL 翚\[[1J\H\\\\R\6m\=m,\q_0)ΰ\\\L&S,UUu]\q4NR\aYm\MXr\y\BU \\z\!8?ϒ0\\\ݞE \WQ\|>\0w\y\_\_\\x6(Z\~%?\\"\0\qlYV^{ӛ\$ضı$\ D\y$\7e+#\\0@$\\8\fR\ \0TUUZV\z^V[* i6\j5\(!1\X\a]y啟\~a\(Ba~~~||\\z\\tTUmZV[\\b \\\Y;Ò\uzD@O?{fj\\}\#Tvm\\l\\Եz\'SN9{۶3\\)\n\jsD\\'B\q_T\\/\r-B\9[\"TPJ(%2g\],Q1Ɠ$\)pg t [h6\0@)\eP_n+jf\n\=\q\u\B\h4t]\v\q\8m;I\r6k\6\0\\J\\/ԧ>w\ލ7W*Lov`vژ\\\;00\&\U/N\"\\\r\i7R>y~\Gƌ;R\\8c[n\\Z,˿|\+䱴P\8f\0/L*\'N?%%h͙~z\g}6\.\t3\"*~\"MB4*i(JR !^e\0\u=Ϸ[\r\r&\4W*\;n1\;\\O<|>EQ\lRzG}txx\q^$\\⹡i,z3[,\"\4!\\J\jAZ{\\\\\t\L\p\D\0X{\z\l|d8.\\8TB\0`:͚٬zȑɋ/: \0a$)\@\L\lRv\\\\zI,\E(ZZjV#T*N377w\{\6\W_}nX1\$i\^{\\\\\~3۶/첱^Dd2M6 \n,\yP%4u`e2ڍ\\\0\\\[\g7ov\E\%\"4MCs \:[R`kɟ\.q IM\R*C\s<ǟ*fv6\4*\0\\py(\`Q\0(\f h+~\D\00n\\$IRU5˅a\O\޽{jj\l6M\4]\}Ǜ\\\%\2\[O<\\7\\\ÇKRT:z\\ÇGFF0A%\$冮j(JŜEٌE\#\CF\w4M\"0 s\\v{|\\.vb\"d \p!8 \͛\W^tS[!d1\\\$q4Sk6\?\o|\[\z\\gL\2L&}\~疷\̳OM;M]d`(l\0\\s5WJ\\g \rv}mH{\gffdYf?ݹs\\\w\6\ZVַ/??z\<ß\g(\\㘟e\l\\_?22\K\D\\n+Q\rz>cLQ4v_WK3\MN䑇<ԣ=?\?a\|\}\7MVt]7T5N\\39;\ϒ\0\0<\0).$\\n.\PGvF%VH\l6;22Q6dI2\\T7\~֟\0\v[\4w\Y͕XJ򴾿%B(=o߾}\\"\o~ӟtX\\iʲ\z^oۆa\8HFGGgffJ\\rZv: .\=\y\ƍgguSk6\\"NU*~ \rT1zI%\u]J4vݽO\qF$\\'HVUog\n%Y?~\{>6M;\\4Y\Z\%! |C\uj\0\\\\<\|>(\nB\\:v\\viB\0 \<=˲\\\FFFm[UՉsCϣ@*\aG$Idi\ΝO=\ѣ\\/~\Kk6o\V>\XCd\ K\ٹ9HPYSH\hbacT\E \~G„,@ ȱ\(\0!N\aڶbhR\\TY Ad\f\'粔),+D*~_N?7b\-IM\TU\\ \\۶\\0qi\-J\\Ô\v],9縻).\nKȢ\\4M e+koݺ\3\X34\f\\nE)\ mL9Z\\O]\\\\?ةۙ4\\\nɊf\YB$\"\0\H%KPƘh8!D\^\\ʃ#@\\\x\ J\0D\0M$\Ze$IDQd\\EAeft!ސ,K2!D8p`Ϟ=۷o\0,c\2\ #\0`F\\|\L&#p]W\u\P\I(U%1M\6lذm󦱁\r\'gQ\r\"B$D\4#n׉SjP \0\n* H%*(0B\"B%\'\uݲ,MӰW\\a,\\\\\u]u]\\U*={\0m\\n7r\_\NDq\UUņ5l1MOO\^۶Q\\t\8.\n\/1\0\\\\۶m[31qmCJ$Q\4u]T*q\0\\\v\'\/|alll~AIx:iA\\&(\00\3 \0u]%a٨5NTJeJB D*S$$\ZQHEtr΄\\8Ƃ\ps\\\=z\>ϵZL&D(<\|\\s\\v\jOh\0|\\/DY΍\\eQ11\F\x(\r20h\ \#\"\\|P*O]-\7mڔ\\{\9Ji\\\ )\=KDK\Xݞ*\8Fqκ\v6*@\"v.݀\i\Z\ac _.,KQL3\n\+k\~\K_:p\eY`\\˷o~\\\"\\qlFݾ\w\څX\Gsγ\lDK\G(\[\l\\BP*D\l\0\t\\(rE\@\\>200yf\BT\۶$\\co1M3dY\zQd١b\\l\\nH@)%@pPlDf\\,\bQiies$Ip;\K\y\\t:[n\uY\˸j\'\"o&\0\G?\IХ\\\|l\>\JDB(\\0`ꆙ !IZL6KYW\%Dn\^˪\\|\4lnB\\\~}\\\/\\AX(TU\j%W\\뮻næS^;\\ˮJG HJ \0%)\0\'\r3Ȳ\ydQ(I\8J\СC7\t\\\دƜp0»jaMSe\q\\.\yRsnf\G[(H Ðeٶ\B0<<\n6aa&ID3\r7% \0084T׳\,a۶k֬,+\\'I266VV\rcIj\bcuP\>|8׽\\{|\\!\0.\np\H\\\A\0@N\rs\;E\=&E\\c\1]H\\F1\uM\\0\w\}IT*d$a\n!ݮ\y^\N7\T\u\u !QIT\l>\3\rYQ4\\pBA\燒\Z\'At]\\dÄ1\8j\\4m~~>In+P(ci>UU9=I\"\0P,{;C)%@E\4\wO\\'`xd\05]W9K(IA$\i:I\x\"x,8\g\AeA\\\"T\bj\C|I\<|?\\O?\/瞧p0$ɶm\q\ߨ=\5ƱȚ5MC\97 öm#{~g{vtt\\ 5nB!IcR\C\ߕSO=\\b 3-J\j<\\4% \6 \r\r\\8f\@\q9rd\ڵӻ\omS\\\|\)\'ljjT\TH\ȧ\FWW\ {G\0\i\*\^\k_\\r7\\\v~w\N8YBӟT\\1eYa\â\\Z%\}\n\ڵ\\ȒZ*V\YVF\m;+˪,\<(L\ N:_kxni\0 \9\\Z?\S\\/\\\0iAP(/\\N0 EQ\U\"\U(pLC\\Ԗ\ \ \$I&\u3jSۻwi_򗯺\f\fэGj⟚\f\A`[e\u5MÂ.\\\VM6]{\v\Zg)΍hZab2\LOO\V߼;\\n}?K/\\n]\k(L\\\s\\[\q[6\`w\s\\\\O(7M\۶iq\\\Z\0HuU\a,)]gWSMR\A&%\"*t\E\"ED \" ^\"55@\\\B\{\|\]gf}3kX\Z\\Y?㿿S@@+)m0R^\m_\L\\$.\\\u=\ABlrVՈ\&mU\RR\WzT)\\\y{\ ~\>\P\0Rjsbg0\ﵬ߿\\\şe`{\H\gE\Q˯`\^Fh\V?K\'}mCM~2b\)\PQnc\>|!4w\҉\"l#\s\n`w@\nF0\\Z?\+\\īB:Q\\2X|@R22\֊84\Z+\(\"_HQ\nVSQ\\YFfz]k\\p\ABAEe`hnUl \!Îi\n2f?!P\\ \ \J\b\\üW\'!Tɔ+/[\=0c!29,}}}x=(j\\\\9jJI\\*6o\^ \JYBzjEÞ\Bo\r\M;[_m\\**\Eed\a <\N UZ\8LWzK\$\a\\_f\\ӌ\sA/v \\k 0R[\\\n6k> \u\\e>H\|\KDzY֠d\\S#%\'n\8diKf} !\V\\mLr4o(Q=5\+\ʑ)+U5A0?J\\n\vnx\L1#gS\\\\\A\rQ5L:a!a\>\\\t\" lZGWyuTo.2bAr\v\ao֋m\Olp\lk\#[gŸ9\qhU\Av\\\\=_\\6T\v\'\U<\\\\\dc`8yL\h\\x_8Ae\\?I\\3G78u\\~\0\\\\30w4@\0YC\\ԙEUm&\s\4<\G\1jtW\\[b~Kx\\7ϪL_\.\'sRY^. #R1\\}Ky<\\⢍w&\0%-6\\\0\\Yr\ZX;\r`>IfCȸ`Uz17n%N \ǀA\6{: +9BSd\bzPŸ\z\\r\fs\ٿ\n\\J-\\d\\\\iF6%(\ltΧ\d\\\\ҒϏ]\|\nY4\\,[\r!P2[JD/\\EG1x~ \:\\\ێ g\T|/a\W}?&Qލv\\\\\\ͳ\ /\‹B&f\s (-\\%uC\z-\A\LM=b\Ou\\j6}q\0\\F`vL\m%&OOR^ yC\ؽ*\\uxP0jE\&\p%\q\r6#\_=8A`>\\N\m^Fa \-ȅ붝\7[eb \n\LZ5>0?E4/\\Pt\AXHҺ+E\c89\9:6,0N\x\q{w?|\YB>\r j4\x7nO\\\\*O\\\d\\LHr÷Þ8\-6nn՘\f.\$\\0\\S\6^\S<|S\2\A\ID`2k\Bڟ\\\Zn\$G492\ \rm)Z҉ \r\p#]]\\r\C\\\{ķ\/Z\pzg!\\3^օ*8 /#\Ѳ\\\r>~q6\\Oy>s# 6Q2e\P,\N&\xڈbI\Tp1PhG\00P^2\ҮSsO\gE|\Z\Z \/F*ȣDҀ d>\hd/w*UUO\1%#YG|\XRq4!V\|!*Fy7\=\-~u\\\\U\\: /\\\s]#\\ H\r+\q˻044{\\irv\;\A|_\Pޙq%?\\"2^\\f$\\_\Z2`n\Z\'\vFF\Au\TP\\mhk3Q\NH\L\ؘ){4\\\3%xo` x\O9\%\ѩOUZ\[\2\N\/GI\>J\s[\\l\\3M4BOg\\v I\rX;-\zq\ZaM\\z)o˭IJNU\\\\\\\E\'?ۣhB7}#\ĨE\C3~yÙ\ɓk\"\\J\P\#C\\ .\$p*\\'߸y ĵ|\Z\\'t\\u4 o4@״\Z2x\n-Z0;?\\ee\[7\xD_IyW\ \]lFM\`\h\ZtKgnT~[Ryj7g\rw0\ZI(\n(\k\s%T\s7\"^\&r\\tV\\qf.H0* \\0@\\.\\\6\\__\Z\cP TR\Jp\FŸĥ1v\#$\Q*Ohl \\\"\"L}FL\u⬩am \a\XpFշBܝq\\Q\;5o\+\ƳM!it(=Ur{=^{~8{X\ض\~d7\^\B\XjvRRyF}\\udCK\N>%`\uQ\[\'>:4\Ok\gљ㻄%\P{2/wG!c O>yC\X\ڸ6\E/wɥ\%c\\'-\:bvHNMVwG\\]\n>΀:\ZdS F\x\\բQgIk\b\@SE!\\\\\\n{=_ٸJ8c7)BuW\\"1~{\Z\K\\R\/w8A6\r\\hdtس\<`5t3x0:M%l߯S\u\\d?:jU\ \Zoꖚ{aFl= [_O\~6\\%5hX\\CihI\\w\Z\eFOG޺W#̎\\]ſϗBu\w\\H-e%)pVZi\6\K4Ĩ F\\ Vm \0pOR=^\\oI}7q6#æe06\\0_M<ّ3OMfe\\\hݣ\g\"\f,\׋gue\r\\n\8\8\:NXq\M;\\u|6,w\ \\\r\\\\O*_k\,])\\n\"E1~ј\Z9RWT7/O >\"\+ r1&\ug\l\a\*-G^\_ڿ\}9f@\e\n#\Ho!\\\V\vJk v\@&.\q&E\*0l\#j\\\r_>]Zɖ\\\\\\0\/(\N~\4Iϒ榑GX^RDn\\G\\3\\Va\ߑWYL+q8\c=?^_ʼNqdq1y\\\;Oj\Mͮ d\V\\q\ە\hذ\Z8633\}LiȦ-a-\"B\Xy+(\^>!\\!S\Y[ ?VZ\'WI\Z\Zc`p/d$\\SYVvvQ3T\ \\~-.n3>a;ӎ\\\\\.Oۚ ܲ &LӻX\\a\\\\t\n\O\_lz鎆n*\=#l#u\e9\\\V=\i=\Mk\v&7mc{0CC \4\eX:]\\V\q\\Yt\x\D\n\\{\>H\Hi\yMf))3ߎm\Z\\\& ;\\v\\\٨2\\#<]i7rX鹕BNL;ZKCp\O=\ҳy ˯̢\Z\ }t\\r !S\8\V%g? ݾ\o&yA\F\L\0*$\xA\@8R޷\زS>wW\Mv\0\q&)t貛(\"ӿ\{G\z;\Em6ϛ\\\,kA%`\Z\P\\T.\b(\\\\rϢԂ2(u&gW[\\m\b\79Űl\XI@^&|\G> v\W*W\\rd\EOGCTZ 5eTF\-\\b)\d\\4eKɨ# \kMD\k\\\0^,ɞ(tL;X&ޡ~4(u\7;龘J^\\4`\.\_7\:M_K%B\2j\"\\rД\\a \Os\\\&Ec%&o\\\_RI1?\ \n\\\G\֛z\TEX\s9\?Q\';> .?rvmt)kbKc[\D\t\ʞ\>i~/ɜGwj\S \\kWi\91.@','REPORTED',_binary 'P$Z\(K\"Ƈ\'),(_binary '\\h2\O\Z\rq4e','2023-06-01 21:44:08',_binary 'gr\ܧH&:\vO\\','2023-06-01 21:44:08',_binary '\Հ\GՏX0 ','second trash',_binary 'x\\\?ӏY\VH\\VDF䒻\rϽ ٌ\\'I.\M\\\ۆܶǧ\~]d\?t\O\\:Ɏ\nD\\\\yw\B |J‚\!+\|cYxHx궗__D\E\\\Jǭ\.ƍc\騐 w\!%NH_=[XӀ\ ˲\>:\\I?ӸU\p\{f,\N\\\H\ 2EO Ռk\ sI:q\X0>#0W_r=+MSyP!&]PO,8(Xx\IIݣ\\ \m¬-\Z=cl(X(\\({WϾKJ\[z\\}\\F2rȚ\I;\\\)O㘔@i\r\$TP\ Ԟ\\\3\\vA쒹\\{cU9^\@h\0\[ 0\x\uO\mJ\\\P\\\\ɑ,r4R\\xNe4\ܽlD\n.}Ê\5I }Vu$sn\v\ʊ!Kr[\rG/šY\\鎎2\\hZ\E\0\s`|\\vB\\t\ܮ\Z,O>Ha\\Z\~7\OТ9A>e{#ι`Ǽ߈\\NOR5\\\\\X\0W\ZJTe&6\]\\'\f\"8UL\/-[.+]\P\br\\Jm\\M\~ל%\Z?}P\|Z\\\\\#\Ї\"O\\ \\kiwF=6[K?AƗ1x9䒧~V\\\"1Ƃ;:l\Qe\Z\u\h\r\\wc\\5\\s\\\f\\IeJDkO\\O!\1\\B–#}N\Y&i\IbUz\\\oQB`#{U\>żlJ\hVũm}i\\u\zqt\\4Ͱ;\\'uJ\2e-TpE\g]V9\"wѦ7\䥽*.\s\i0.̓ J}g\ܩZWyU\Um+3Q\Znwj*丙\ +?^ f:\8J Q2\U}\\Pަҏ\\ \|\q\\χ%z\\|KV\;0\P\WۧP\\PWE\j\*YQR\M\2\'\BE./Ok Yc\:bt \?*Ya\&.\6#c\G\1e,\\Z}\<\\n\N)$ \W\0`]q \0ks\+|f\&895վH\\Y~8=A\dN\|\\0! 7\ҰC.r\\:\0\[\\Z\rZJ\\0[(UuGFX\#axJ\g?\"8NveB\\McM\ҲXD΁\\"\{I; sv\Zw\դ\i\Ů\la\0\0; h\0\'O\\"\+o\h\\t\\󈄨\0&\:k@䣈\}mFGfqFE\JB`01w\D\zq\"[փu !N\m\z*\aVI\\D9\n\(JAe\\#\6=4\oP\'\\0?ϫ\\"|}u]&\' {A;(\\ef\\\'O\C\Fo\؛dme\\E12q&ٺPC>\eGńn\{j\\=t\C\=t\ES\%,\}z:zE\P[+M1w\c\\r\\\\'Ν\\ܩ\\7\\ǿp x=\\\O8\h]\\7ͥ\Zݖ6Vb&pc\0N֦,ܥ\n8\n.]T5 \@v\REz]_\\\V\ a7UH\uhF\n \5\&\\\ޟBRfM\'ؑ\0\*G\n9§fF3\uf\:\\I?ӸU\p\{f,\N\\\H\ 2EO Ռk\ sI:q\X0>#0W_r=+MSyP!&]PO,8(Xx\IIݣ\\ \m¬-\Z=cl(X(\\({WϾKJ\[z\\}\\F2rȚ\I;\\\)O㘔@i\r\$TP\ Ԟ\\\3\\vA쒹\\{cU9^\@h\0\[ 0\x\uO\mJ\\\P\\\\ɑ,r4R\\xNe4\ܽlD\n.}Ê\5I }Vu$sn\v\ʊ!Kr[\rG/šY\\鎎2\\hZ\E\0\s`|\\vB\\t\ܮ\Z,O>Ha\\Z\~7\OТ9A>e{#ι`Ǽ߈\\NOR5\\\\\X\0W\ZJTe&6\]\\'\f\"8UL\/-[.+]\P\br\\Jm\\M\~ל%\Z?}P\|Z\\\\\#\Ї\"O\\ \\kiwF=6[K?AƗ1x9䒧~V\\\"1Ƃ;:l\Qe\Z\u\h\r\\wc\\5\\s\\\f\\IeJDkO\\O!\1\\B–#}N\Y&i\IbUz\\\oQB`#{U\>żlJ\hVũm}i\\u\zqt\\4Ͱ;\\'uJ\2e-TpE\g]V9\"wѦ7\䥽*.\s\i0.̓ J}g\ܩZWyU\Um+3Q\Znwj*丙\ +?^ f:\8J Q2\U}\\Pަҏ\\ \|\q\\χ%z\\|KV\;0\P\WۧP\\PWE\j\*YQR\M\2\'\BE./Ok Yc\:bt \?*Ya\&.\6#c\G\1e,\\Z}\<\\n\N)$ \W\0`]q \0ks\+|f\&895վH\\Y~8=A\dN\|\\0! 7\ҰC.r\\:\0\[\\Z\rZJ\\0[(UuGFX\#axJ\g?\"8NveB\\McM\ҲXD΁\\"\{I; sv\Zw\դ\i\Ů\la\0\0; h\0\'O\\"\+o\h\\t\\󈄨\0&\:k@䣈\}mFGfqFE\JB`01w\D\zq\"[փu !N\m\z*\aVI\\D9\n\(JAe\\#\6=4\oP\'\\0?ϫ\\"|}u]&\' {A;(\\ef\\\'O\C\Fo\؛dme\\E12q&ٺPC>\eGńn\{j\\=t\C\=t\ES\%,\}z:zE\P[+M1w\c\\r\\\\'Ν\\ܩ\\7\\ǿp x=\\\O8\h]\\7ͥ\Zݖ6Vb&pc\0N֦,ܥ\n8\n.]T5 \@v\REz]_\\\V\ a7UH\uhF\n \5\&\\\ޟBRfM\'ؑ\0\*G\n9§fF3\uf\4~N)AٽlW','2023-05-29 20:59:15',_binary 'gr\ܧH&:\vO\\','2023-05-29 20:59:15',_binary '\Հ\GՏX0 ','csabavadasz79@gmail.com','Csaba','$2a$10$IHGHLrIwDrapqOl9sAFXVeQZJNCw11P.QhzDoACnNVZInaM9EV4a2','ROLE_USER'),(_binary 'IމH/\"9G\','2023-06-04 17:27:58',_binary 'gr\ܧH&:\vO\\','2023-06-04 17:27:58',_binary '\Հ\GՏX0 ','test@testemail.com','Test','$2a$10$RVyObuY7eTQXYkoQV4he3O6GwxUjbAI3o3Ul0tNTLFbDvHIU8GVcK','ROLE_USER'),(_binary 'jĺ6BN_Tl','2023-06-01 19:13:00',_binary 'gr\ܧH&:\vO\\','2023-06-01 19:13:00',_binary '\Հ\GՏX0 ','verejanvasile@gmail.com','Vasile','$2a$10$nN6jg4sEjhTZvhuZunSyt.n8DLaCQuOECbQf/bKCXwJJ/8vIfYqJu','ROLE_USER'),(_binary '\\!\\AЀ+_ݜ\','2023-06-02 12:07:42',_binary 'gr\ܧH&:\vO\\','2023-06-02 12:07:42',_binary '\Հ\GՏX0 ','eugene@gmail.com','Eugene','$2a$10$H/5j0TlgWVGB7fbxCXodo.3/dzK41Eo/NPPpb.8VikNOaOWU0QaLO','ROLE_USER'),(_binary '\@:YEA\sqd,G','2023-06-04 18:33:41',_binary 'gr\ܧH&:\vO\\','2023-06-04 18:33:41',_binary '\Հ\GՏX0 ','regtest@gmail.com','RegistrationForm','$2a$10$M90LdTFXODRC3dsEITH2Q.Yle1ChFVwQzefzTHZjvRQRgfKA6PYIy','ROLE_USER'),(_binary '1GL^fmQR','2023-06-04 18:42:59',_binary 'gr\ܧH&:\vO\\','2023-06-04 18:42:59',_binary '\Հ\GՏX0 ','redirectlogin@gmail.com','RedirectToLoginAfterReg','$2a$10$Up7Dzc5idkyftAVE/2N0Lef3f.dBfiws2mpO/HuGkTb8kuyg8vARy','ROLE_USER'),(_binary 's{A\Ks\" Ԁ','2023-06-05 20:28:28',_binary 'gr\ܧH&:\vO\\','2023-06-05 20:28:28',_binary '\Հ\GՏX0 ','showvasile@test.com','TestForVasile','$2a$10$NugFZcqRsKItoiRMWFA37ud/e6ud6v31nDYt2Z/ydSgv5Zbwk6C1m','ROLE_USER'); +/*!40000 ALTER TABLE `litter_snap_user` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2023-06-05 23:05:04 diff --git a/doc/database/eerdiagram/litterSnapEerDiagram.png b/doc/database/eerdiagram/litterSnapEerDiagram.png new file mode 100644 index 0000000..a33fd9f Binary files /dev/null and b/doc/database/eerdiagram/litterSnapEerDiagram.png differ diff --git a/doc/database/script/litter_snap_database_create_script.sql b/doc/database/script/litter_snap_database_create_script.sql new file mode 100644 index 0000000..b11b95d --- /dev/null +++ b/doc/database/script/litter_snap_database_create_script.sql @@ -0,0 +1 @@ +CREATE DATABASE litter_snap CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; \ No newline at end of file diff --git a/doc/mvp2-flowchart/class-diagram/address_model.png b/doc/mvp2-flowchart/class-diagram/address_model.png new file mode 100644 index 0000000..afa5d1d Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/address_model.png differ diff --git a/doc/mvp2-flowchart/class-diagram/controllers.png b/doc/mvp2-flowchart/class-diagram/controllers.png new file mode 100644 index 0000000..aeb2ae8 Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/controllers.png differ diff --git a/doc/mvp2-flowchart/class-diagram/entities.png b/doc/mvp2-flowchart/class-diagram/entities.png new file mode 100644 index 0000000..bd1d5fe Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/entities.png differ diff --git a/doc/mvp2-flowchart/class-diagram/enums.png b/doc/mvp2-flowchart/class-diagram/enums.png new file mode 100644 index 0000000..b0d525d Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/enums.png differ diff --git a/doc/mvp2-flowchart/class-diagram/litter_models.png b/doc/mvp2-flowchart/class-diagram/litter_models.png new file mode 100644 index 0000000..ef57305 Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/litter_models.png differ diff --git a/doc/mvp2-flowchart/class-diagram/repositories.png b/doc/mvp2-flowchart/class-diagram/repositories.png new file mode 100644 index 0000000..def4161 Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/repositories.png differ diff --git a/doc/mvp2-flowchart/class-diagram/services.png b/doc/mvp2-flowchart/class-diagram/services.png new file mode 100644 index 0000000..983bf05 Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/services.png differ diff --git a/doc/mvp2-flowchart/class-diagram/user_models.png b/doc/mvp2-flowchart/class-diagram/user_models.png new file mode 100644 index 0000000..aab1b10 Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/user_models.png differ diff --git a/doc/mvp2-flowchart/class-diagram/util_classes_static_methods.png b/doc/mvp2-flowchart/class-diagram/util_classes_static_methods.png new file mode 100644 index 0000000..7213231 Binary files /dev/null and b/doc/mvp2-flowchart/class-diagram/util_classes_static_methods.png differ diff --git a/doc/mvp2-flowchart/flowchart.jpg b/doc/mvp2-flowchart/flowchart.jpg new file mode 100644 index 0000000..f0c715e Binary files /dev/null and b/doc/mvp2-flowchart/flowchart.jpg differ diff --git a/doc/mvp2-flowchart/litter-snap-mvp2-report.docx b/doc/mvp2-flowchart/litter-snap-mvp2-report.docx new file mode 100644 index 0000000..f7f72ab Binary files /dev/null and b/doc/mvp2-flowchart/litter-snap-mvp2-report.docx differ diff --git a/doc/postman/litter-snap.postman_collection.json b/doc/postman/litter-snap.postman_collection.json new file mode 100644 index 0000000..f5c2eb1 --- /dev/null +++ b/doc/postman/litter-snap.postman_collection.json @@ -0,0 +1,448 @@ +{ + "info": { + "_postman_id": "79b7b733-8af9-4577-8113-9e9502810153", + "name": "litter-snap", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "18091890", + "_collection_link": "https://crimson-firefly-974571.postman.co/workspace/My-Workspace~7e2291dc-a68f-4c99-8e2b-6a4ed4564dfd/collection/18091890-79b7b733-8af9-4577-8113-9e9502810153?action=share&creator=18091890&source=collection_link" + }, + "item": [ + { + "name": "TestEmail", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "llpjrdsbulhtsxfr", + "type": "string" + }, + { + "key": "username", + "value": "info.littersnap@gmail.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "username", + "value": "info.littersnap@gmail.com", + "type": "text" + }, + { + "key": "password", + "value": "llpjrdsbulhtsxfr", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"emailTo\": \"csabavadasz79@gmail.com\",\r\n \"subject\": \"Test Subject\",\r\n \"body\": \"Test Body\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/email/test", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "email", + "test" + ] + } + }, + "response": [] + }, + { + "name": "Render All Users", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8081/api/auth/admin/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Render All Litters", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8081/api/auth/admin/litters", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "litters" + ] + } + }, + "response": [] + }, + { + "name": "Render Litter By Id", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "description", + "value": "Test from postman", + "type": "text" + }, + { + "key": "firstLine", + "value": "Rózsa utca 40/A", + "type": "text" + }, + { + "key": "postCode", + "value": "1077", + "type": "text" + }, + { + "key": "city", + "value": "Budapest", + "type": "text" + }, + { + "key": "country", + "value": "Hungary", + "type": "text" + }, + { + "key": "file", + "type": "file", + "src": "/C:/Users/Computer/Desktop/LitterSnap/VasileKevinClientPics/LitterSnapTwins.png" + } + ] + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/litters/06ca48b1-bc33-4237-a42c-690c69416506", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "litters", + "06ca48b1-bc33-4237-a42c-690c69416506" + ] + } + }, + "response": [] + }, + { + "name": "Render All Reports", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "description", + "value": "Test from postman", + "type": "text" + }, + { + "key": "firstLine", + "value": "Rózsa utca 40/A", + "type": "text" + }, + { + "key": "postCode", + "value": "1077", + "type": "text" + }, + { + "key": "city", + "value": "Budapest", + "type": "text" + }, + { + "key": "country", + "value": "Hungary", + "type": "text" + }, + { + "key": "file", + "type": "file", + "src": "/C:/Users/Computer/Desktop/LitterSnap/VasileKevinClientPics/LitterSnapTwins.png" + } + ] + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/reports", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "reports" + ] + } + }, + "response": [] + }, + { + "name": "Add new litter with photo", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "description", + "value": "Test from postman", + "type": "text" + }, + { + "key": "firstLine", + "value": "Rózsa utca 40/A", + "type": "text" + }, + { + "key": "postCode", + "value": "1077", + "type": "text" + }, + { + "key": "city", + "value": "Budapest", + "type": "text" + }, + { + "key": "country", + "value": "Hungary", + "type": "text" + }, + { + "key": "file", + "type": "file", + "src": "/C:/Users/Computer/Desktop/LitterSnap/VasileKevinClientPics/LitterSnapTwins.png" + } + ] + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/litters", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "litters" + ] + } + }, + "response": [] + }, + { + "name": "Add New User", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"eugene@gmail.com\",\r\n \"firstName\": \"Eugene\",\r\n \"password\": \"Almafa1234?\",\r\n \"passwordConfirmation\": \"Almafa1234?\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Add Vasile", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"verejanvasile@gmail.com\",\r\n \"firstName\": \"Vasile\",\r\n \"password\": \"Almafa1234?\",\r\n \"passwordConfirmation\": \"Almafa1234?\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Add Kevin", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"kevinzefi1999@gmail.com\",\r\n \"firstName\": \"Kevin\",\r\n \"password\": \"Almafa1234?\",\r\n \"passwordConfirmation\": \"Almafa1234?\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Modify Existing User", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"kevinkevin@gmail.com\",\r\n \"firstName\": \"Csaba\",\r\n \"password\": \"Almafa1234?\",\r\n \"passwordConfirmation\": \"Almafa1234?\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/users/fd4fb0b7-933e-4765-9856-cac7410701fe", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "users", + "fd4fb0b7-933e-4765-9856-cac7410701fe" + ] + } + }, + "response": [] + }, + { + "name": "Delete An Existing User", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"csaba.vadasz79@gmail.com\",\r\n \"firstName\": \"Csaba\",\r\n \"password\": \"Almafa1234?\",\r\n \"passwordConfirmation\": \"Almafa1234?\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/api/auth/admin/users/8a2d5556-d155-469d-862b-bad563ba0abb", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "api", + "auth", + "admin", + "users", + "8a2d5556-d155-469d-862b-bad563ba0abb" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/doc/screenshots-of-working-application/001_index.png b/doc/screenshots-of-working-application/001_index.png new file mode 100644 index 0000000..dd12e7c Binary files /dev/null and b/doc/screenshots-of-working-application/001_index.png differ diff --git a/doc/screenshots-of-working-application/002_login.png b/doc/screenshots-of-working-application/002_login.png new file mode 100644 index 0000000..e89b17b Binary files /dev/null and b/doc/screenshots-of-working-application/002_login.png differ diff --git a/doc/screenshots-of-working-application/003_registration.png b/doc/screenshots-of-working-application/003_registration.png new file mode 100644 index 0000000..6c9dff7 Binary files /dev/null and b/doc/screenshots-of-working-application/003_registration.png differ diff --git a/doc/screenshots-of-working-application/004_litter_reports.png b/doc/screenshots-of-working-application/004_litter_reports.png new file mode 100644 index 0000000..3ebe28a Binary files /dev/null and b/doc/screenshots-of-working-application/004_litter_reports.png differ diff --git a/doc/screenshots-of-working-application/005_view_litter_report.png b/doc/screenshots-of-working-application/005_view_litter_report.png new file mode 100644 index 0000000..7e804a2 Binary files /dev/null and b/doc/screenshots-of-working-application/005_view_litter_report.png differ diff --git a/doc/screenshots-of-working-application/006_modify_litter_report.png b/doc/screenshots-of-working-application/006_modify_litter_report.png new file mode 100644 index 0000000..53c05fb Binary files /dev/null and b/doc/screenshots-of-working-application/006_modify_litter_report.png differ diff --git a/doc/screenshots-of-working-application/007_view_users.png b/doc/screenshots-of-working-application/007_view_users.png new file mode 100644 index 0000000..646eb8c Binary files /dev/null and b/doc/screenshots-of-working-application/007_view_users.png differ diff --git a/doc/screenshots-of-working-application/008_view_users_with_button.png b/doc/screenshots-of-working-application/008_view_users_with_button.png new file mode 100644 index 0000000..9e22b2b Binary files /dev/null and b/doc/screenshots-of-working-application/008_view_users_with_button.png differ diff --git a/doc/screenshots-of-working-application/009_add_litter_by_adding_city_press_button.png b/doc/screenshots-of-working-application/009_add_litter_by_adding_city_press_button.png new file mode 100644 index 0000000..6bacc3a Binary files /dev/null and b/doc/screenshots-of-working-application/009_add_litter_by_adding_city_press_button.png differ diff --git a/doc/screenshots-of-working-application/010_add_litter_and_city_filled_on_form.png b/doc/screenshots-of-working-application/010_add_litter_and_city_filled_on_form.png new file mode 100644 index 0000000..ddf4c94 Binary files /dev/null and b/doc/screenshots-of-working-application/010_add_litter_and_city_filled_on_form.png differ diff --git a/doc/screenshots-of-working-application/011_error_page.png b/doc/screenshots-of-working-application/011_error_page.png new file mode 100644 index 0000000..5a68841 Binary files /dev/null and b/doc/screenshots-of-working-application/011_error_page.png differ diff --git a/pom.xml b/pom.xml index 70bbcc4..65fd9b9 100644 --- a/pom.xml +++ b/pom.xml @@ -48,11 +48,6 @@ spring-boot-starter-mail - - org.thymeleaf.extras - thymeleaf-extras-springsecurity6 - - org.springframework.boot spring-boot-devtools @@ -90,12 +85,6 @@ 2.19.0 - - org.springframework.boot - spring-boot-starter-test - test - - org.junit.jupiter junit-jupiter-engine @@ -103,13 +92,24 @@ test - + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + - - diff --git a/src/main/java/com/csaba79coder/littersnap/LitterSnapApplication.java b/src/main/java/com/csaba79coder/littersnap/LitterSnapApplication.java index 3b09fc3..210d9af 100644 --- a/src/main/java/com/csaba79coder/littersnap/LitterSnapApplication.java +++ b/src/main/java/com/csaba79coder/littersnap/LitterSnapApplication.java @@ -1,19 +1,20 @@ package com.csaba79coder.littersnap; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * The main class of the application. + */ @SpringBootApplication -public class LitterSnapApplication implements ApplicationRunner { +public class LitterSnapApplication { + /** + * The main method of the application. + * + * @param args The command line arguments. + */ public static void main(String[] args) { SpringApplication.run(LitterSnapApplication.class, args); } - - @Override - public void run(ApplicationArguments args) throws Exception { - - } } diff --git a/src/main/java/com/csaba79coder/littersnap/config/SecurityConfiguration.java b/src/main/java/com/csaba79coder/littersnap/config/SecurityConfiguration.java index 1f2110a..57ac381 100644 --- a/src/main/java/com/csaba79coder/littersnap/config/SecurityConfiguration.java +++ b/src/main/java/com/csaba79coder/littersnap/config/SecurityConfiguration.java @@ -12,15 +12,31 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +/** + * This is a temporary solution to provide a security configuration for the application. + */ @Configuration @EnableWebSecurity public class SecurityConfiguration { + /** + * This is a temporary solution to provide a password encoder for the application. + * @return + * @see Spring Security documentation + * @see Spring Security Registration Password Encoding with Bcrypt + * @see Spring Security Registration Password Encoding with Bcrypt + */ @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } + /** + * This is a temporary solution to provide a security filter chain for the application. + * @param http + * @return + * @throws Exception + */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http @@ -36,25 +52,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .and() .csrf().disable(); // Disable CSRF protection return http.build(); - - /* - this disables all authentication and authorization for the moment! - http - .authorizeRequests() - .anyRequest().permitAll() // Allow access to all endpoints - .and() - .formLogin() - .disable() // Disable form-based login - .and() - .logout() - .disable() // Disable logout functionality - .and() - .csrf() - .disable(); // Disable CSRF protection - return http.build(); - */ } + /** + * This is a temporary solution to provide a user for the application. + * This is not a good practice, but it is enough for the moment. + *

+ */ @Bean public UserDetailsService users() { UserDetails user = User.builder() diff --git a/src/main/java/com/csaba79coder/littersnap/controller/EmailController.java b/src/main/java/com/csaba79coder/littersnap/controller/EmailController.java index 3fba3ff..67e32e2 100644 --- a/src/main/java/com/csaba79coder/littersnap/controller/EmailController.java +++ b/src/main/java/com/csaba79coder/littersnap/controller/EmailController.java @@ -10,13 +10,27 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * EmailController + */ @RestController @RequiredArgsConstructor @RequestMapping("/api/auth") public class EmailController { + /** + * The litterSnapEmailSenderService field. + *

+ * This field is used to send emails. + *

+ */ private final LitterSnapEmailSenderServiceImpl litterSnapEmailSenderService; + /** + * This method is used to send a test email. + * @param emailRequest the email request + * @return the response entity + */ @PostMapping(value = "/admin/email/test", produces = "application/json") public ResponseEntity sendTestEmail(@RequestBody EmailRequest emailRequest) { diff --git a/src/main/java/com/csaba79coder/littersnap/controller/LitterController.java b/src/main/java/com/csaba79coder/littersnap/controller/LitterController.java index a63f66e..70db52d 100644 --- a/src/main/java/com/csaba79coder/littersnap/controller/LitterController.java +++ b/src/main/java/com/csaba79coder/littersnap/controller/LitterController.java @@ -7,7 +7,16 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +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.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -18,39 +27,71 @@ @RequiredArgsConstructor public class LitterController { - // TODO: render all litters for the logged in user only! - // TODO: create findByUserId method in ReportRepository - + /** + * The LitterService. + * TODO: render all litters for the logged-in user only! + * TODO: create findByUserId method in ReportRepository + */ private final LitterService litterService; + /** + * This method is used to render all litters. + * @return the list of litters + */ @GetMapping("/admin/litters") public List renderAllLitters() { return litterService.findAllLitters(); } - @PostMapping("/admin/litters") - public ResponseEntity addNewLitter(@RequestBody LitterCreateOrModifyModel litterModel, - @RequestBody Address address, + /** + * This method is used to add a new litter. + * @param litterModel the litter model + * @param address the address + * @param file the file + * @return the response entity + */ + @PostMapping(value = "/admin/litters", + consumes = "multipart/form-data", + produces = "application/json") + public ResponseEntity addNewLitter(@ModelAttribute LitterCreateOrModifyModel litterModel, + @ModelAttribute Address address, @RequestParam("file") MultipartFile file) { return ResponseEntity.status(201).body(litterService.addNewLitter(litterModel, address, file)); } + /** + * This method is used to render a litter by id. + * @param id the id + * @return the response entity + */ @GetMapping("/admin/litters/{id}") public ResponseEntity renderLitterById(@PathVariable("id") UUID id) { LitterModel litter = litterService.findLitterById(id); if (litter != null) { return new ResponseEntity<>(litter, HttpStatus.OK); - } return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - @PutMapping("/admin/litters/modify/{id}") + /** + * This method is used to update a litter by id. + * @param id the id + * @param model the model + * @return the response entity + */ + @PutMapping(value = "/admin/litters/modify/{id}", + consumes = "application/json", + produces = "application/json") public ResponseEntity updateExistingLitter(@PathVariable("id") UUID id, @RequestBody LitterCreateOrModifyModel model) { return ResponseEntity.status(200).body(litterService.modifyAnExistingLitter(id, model)); } + /** + * This method is used to delete a litter by id. + * @param id the id + * @return the response entity + */ @DeleteMapping("/admin/litters/delete/{id}") public ResponseEntity deleteExistingLitter(@PathVariable("id") UUID id) { litterService.deleteAnExistingLitter(id); diff --git a/src/main/java/com/csaba79coder/littersnap/controller/ReportController.java b/src/main/java/com/csaba79coder/littersnap/controller/ReportController.java index 2a052a3..400d555 100644 --- a/src/main/java/com/csaba79coder/littersnap/controller/ReportController.java +++ b/src/main/java/com/csaba79coder/littersnap/controller/ReportController.java @@ -2,34 +2,52 @@ import com.csaba79coder.littersnap.model.report.dto.ReportModel; import com.csaba79coder.littersnap.model.report.service.ReportService; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; +/** + * The ReportController. + */ @RestController +@RequiredArgsConstructor @RequestMapping("/api/auth") public class ReportController { - // TODO: render all reports for the logged in user only! - // TODO: create findByUserId method in ReportRepository + /** + * Dependency injection for the ReportService. + * TODO: render all reports for the logged in user only! + * TODO: create findByUserId method in ReportRepository + */ private final ReportService reportService; - public ReportController(ReportService reportService) { - this.reportService = reportService; - } - + /** + * This method is used to render all reports. + * @return the list of reports, and status code 200 (OK) + */ @GetMapping("/admin/reports") - public List renderAllReports() { - return reportService.findAllReports(); + public ResponseEntity> renderAllReports() { + return ResponseEntity.status(200).body(reportService.findAllReports()); } + /** + * This method is used to add a new report. + * @param model the report model + * @return the response entity and status code 201 (Created) + */ @PostMapping("/admin/reports") public ResponseEntity addNewReport(@RequestBody ReportModel model) { return ResponseEntity.status(201).body(reportService.addNewReport(model)); } + /** + * This method is used to render a report by its id. + * @param id the id + * @return the response entity and status code 200 (OK) + */ @GetMapping("/admin/reports/{id}") public ResponseEntity renderReportById(@PathVariable("id") UUID id) { ReportModel report = reportService.findReportById(id); @@ -39,11 +57,22 @@ public ResponseEntity renderReportById(@PathVariable("id") UUID id) return ResponseEntity.notFound().build(); // Return 404 (Not Found) status } + /** + * This method is used to update an existing report. + * @param id the id + * @param model the report model + * @return the response entity and status code 200 (OK) + */ @PutMapping("/admin/reports/{id}") public ResponseEntity updateExistingReport(@PathVariable("id") UUID id, @RequestBody ReportModel model) { return ResponseEntity.status(200).body(reportService.modifyAnExistingReport(id, model)); } + /** + * This method is used to delete an existing report. + * @param id the id + * @return status code 204 (No Content) if successful + */ @DeleteMapping("/admin/reports/{id}") public ResponseEntity deleteExistingReport(@PathVariable("id") UUID id) { reportService.deleteExistingReport(id); diff --git a/src/main/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandler.java b/src/main/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandler.java index 4991235..03360fa 100644 --- a/src/main/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandler.java +++ b/src/main/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandler.java @@ -2,7 +2,6 @@ import com.csaba79coder.littersnap.value.ErrorCode; import jakarta.mail.SendFailedException; -import org.modelmapper.ValidationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -18,34 +17,75 @@ import static com.csaba79coder.littersnap.value.ErrorCode.LS_004; import static com.csaba79coder.littersnap.value.ErrorCode.LS_005; +/** + * This class contains the exception handlers for the application. + */ @ControllerAdvice public class ControllerExceptionHandler { + /** + * Handles NoSuchElementException. + * + * @param ex the exception to handle + * @return the response entity with the error message + */ @ExceptionHandler(value = {NoSuchElementException.class}) public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { return new ResponseEntity<>(responseBodyWithMessage(LS_001, ex.getMessage()), HttpStatus.NOT_FOUND); } + /** + * Handles InputMismatchException. + * + * @param ex the exception to handle + * @return the response entity with the error message + */ @ExceptionHandler(value = {InputMismatchException.class}) public ResponseEntity handleInvalidInputException(InputMismatchException ex) { return new ResponseEntity<>(responseBodyWithMessage(LS_002, ex.getMessage()), HttpStatus.BAD_REQUEST); } - @ExceptionHandler(value = {ValidationException.class}) - public ResponseEntity handleInvalidPasswordOrUsernameException(ValidationException ex) { - return new ResponseEntity<>(responseBodyWithMessage(LS_003, ex.getMessage()), HttpStatus.BAD_REQUEST); + /** + * Handles MyValidationException with a single string message. + * + * @param ex the exception to handle + * @return the response entity with the error message + */ + @ExceptionHandler(value = {MyValidationException.class}) + public ResponseEntity handleInvalidPasswordOrUsernameException(MyValidationException ex) { + String message = ex.getMessage(); + return new ResponseEntity<>(responseBodyWithMessage(LS_003, message), HttpStatus.BAD_REQUEST); } + /** + * Handles SendFailedException. + * + * @param ex the exception to handle + * @return the response entity with the error message + */ @ExceptionHandler(value = {SendFailedException.class}) public ResponseEntity handleSendFailedException(SendFailedException ex) { return new ResponseEntity<>(responseBodyWithMessage(LS_004, ex.getMessage()), HttpStatus.BAD_REQUEST); } + /** + * Handles IllegalArgumentException. + * + * @param ex the exception to handle + * @return the response entity with the error message + */ @ExceptionHandler(value = {IllegalArgumentException.class}) public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { return new ResponseEntity<>(responseBodyWithMessage(LS_005, ex.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } + /** + * Creates the response body with the error message. + * + * @param code the error code + * @param message the error message + * @return the response body with the error message + */ private String responseBodyWithMessage(ErrorCode code, String message) { return Map.of(code, message).toString(); } diff --git a/src/main/java/com/csaba79coder/littersnap/exception/MyValidationException.java b/src/main/java/com/csaba79coder/littersnap/exception/MyValidationException.java new file mode 100644 index 0000000..9c2c984 --- /dev/null +++ b/src/main/java/com/csaba79coder/littersnap/exception/MyValidationException.java @@ -0,0 +1,17 @@ +package com.csaba79coder.littersnap.exception; + +/** + * This class contains a custom exception for validation errors. + */ +public class MyValidationException extends RuntimeException { + + /** + * Constructor for MyValidationException. + * + * @param message the message to be displayed + * checking email and password if it is valid. + */ + public MyValidationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/csaba79coder/littersnap/model/address/dto/AddressModel.java b/src/main/java/com/csaba79coder/littersnap/model/address/dto/AddressModel.java index 7d55b75..f2d4ec5 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/address/dto/AddressModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/address/dto/AddressModel.java @@ -2,12 +2,25 @@ import lombok.*; +/** + * This class contains the address model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class AddressModel { + /** + * The address model fields. + *

+ * id: the address id + * firstLine: the first line of the address + * postCode: the post code of the address + * city: the city of the address + * country: the country of the address + *

+ */ private String firstLine; private String postCode; private String city; diff --git a/src/main/java/com/csaba79coder/littersnap/model/address/entity/Address.java b/src/main/java/com/csaba79coder/littersnap/model/address/entity/Address.java index 49630cf..8a846d9 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/address/entity/Address.java +++ b/src/main/java/com/csaba79coder/littersnap/model/address/entity/Address.java @@ -10,6 +10,9 @@ import java.util.List; +/** + * This class contains the address entity. + */ @Entity @Table(name = "address") @Getter @@ -18,6 +21,24 @@ @NoArgsConstructor public class Address extends Auditable { + /** + * The address entity fields. + *

+ * id: the address id + * firstLine: the first line of the address + * postCode: the post code of the address + * city: the city of the address + * country: the country of the address + *

+ * Extends Auditable class, that extends Identifier class, which contains the following fields: + *

+ * createdAt: the date when the entity was created + * updatedAt: the date when the entity was last modified + * createdBy: the user who created the entity + * updatedBy: the user who last modified the entity + * id: the address id (UUID) - this is the primary key and auto-generated + *

+ */ @Column(name = "first_line") private String firstLine; @@ -30,6 +51,12 @@ public class Address extends Auditable { @Column(name = "country") private String country; + /** + * The address entity relationships. + *

+ * litters: the list of litters associated with the address + *

+ */ @OneToMany(mappedBy = "address") private List litters; } diff --git a/src/main/java/com/csaba79coder/littersnap/model/address/persitence/AddressRepository.java b/src/main/java/com/csaba79coder/littersnap/model/address/persitence/AddressRepository.java index 519bf85..777ceaf 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/address/persitence/AddressRepository.java +++ b/src/main/java/com/csaba79coder/littersnap/model/address/persitence/AddressRepository.java @@ -7,9 +7,21 @@ import java.util.Optional; import java.util.UUID; +/** + * This class contains the address repository. + */ @Repository public interface AddressRepository extends JpaRepository { + /** + * This method finds an address by city, country, post code and first line. + * + * @param city the city of the address + * @param country the country of the address + * @param postCode the post code of the address + * @param firstLine the first line of the address + * @return the address + */ Optional
findAddressByCityContainingIgnoreCaseAndCountryContainingIgnoreCaseAndPostCodeAndFirstLineContainsIgnoreCase( String city, String country, String postCode, String firstLine); } diff --git a/src/main/java/com/csaba79coder/littersnap/model/base/entity/Auditable.java b/src/main/java/com/csaba79coder/littersnap/model/base/entity/Auditable.java index dea2d04..c832f90 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/base/entity/Auditable.java +++ b/src/main/java/com/csaba79coder/littersnap/model/base/entity/Auditable.java @@ -9,11 +9,25 @@ import java.time.LocalDateTime; import java.util.UUID; +/** + * This class contains the auditable entity. + */ @MappedSuperclass @Getter @Setter public class Auditable extends Identifier { + /** + * The auditable entity fields. + *

+ * createdAt: the date and time when the entity was created + * updatedAt: the date and time when the entity was updated + * createdBy: the user who created the entity + * updatedBy: the user who updated the entity + *

+ * extended from Identifier: + * id: the auditable id + */ @CreationTimestamp @Column(name = "created_at", nullable = false) private LocalDateTime createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/csaba79coder/littersnap/model/base/entity/Identifier.java b/src/main/java/com/csaba79coder/littersnap/model/base/entity/Identifier.java index bd6f3b9..dc8f5e5 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/base/entity/Identifier.java +++ b/src/main/java/com/csaba79coder/littersnap/model/base/entity/Identifier.java @@ -9,10 +9,19 @@ import java.util.UUID; +/** + * This class contains the identifier entity. + */ @MappedSuperclass @Getter public class Identifier { + /** + * The identifier entity fields. + *

+ * id: the identifier id + *

+ */ @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(name = "id", nullable = false) diff --git a/src/main/java/com/csaba79coder/littersnap/model/email/entity/EmailRequest.java b/src/main/java/com/csaba79coder/littersnap/model/email/entity/EmailRequest.java index fc01026..2a30fe3 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/email/entity/EmailRequest.java +++ b/src/main/java/com/csaba79coder/littersnap/model/email/entity/EmailRequest.java @@ -5,12 +5,23 @@ import lombok.NoArgsConstructor; import lombok.Setter; +/** + * This class contains the email request entity. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class EmailRequest { + /** + * The email request entity fields. + *

+ * emailTo: the email address to send the email to + * subject: the subject of the email + * body: the body of the email + *

+ */ private String emailTo; private String subject; private String body; diff --git a/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderService.java b/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderService.java index a24ee0a..2ab7c39 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderService.java +++ b/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderService.java @@ -3,8 +3,17 @@ import com.csaba79coder.littersnap.model.email.entity.EmailRequest; import org.springframework.stereotype.Service; +/** + * This class contains the litter snap email sender service. + */ @Service public interface LitterSnapEmailSenderService { + /** + * This method sends an email. + * + * @param emailRequest the email request + * @return true if the email was sent, false otherwise + */ boolean sendEmailHtml(EmailRequest emailRequest); } diff --git a/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderServiceImpl.java b/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderServiceImpl.java index 55028bc..1048345 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderServiceImpl.java +++ b/src/main/java/com/csaba79coder/littersnap/model/email/service/LitterSnapEmailSenderServiceImpl.java @@ -9,13 +9,29 @@ import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +/** + * This class contains the litter snap email sender service implementation. + * Also include logs errors and exceptions. + */ @Service("email") @RequiredArgsConstructor @Slf4j public class LitterSnapEmailSenderServiceImpl implements LitterSnapEmailSenderService { + /** + * The litter snap email sender service fields. + *

+ * mailSender: the mail sender + *

+ */ private final JavaMailSender mailSender; + /** + * This method sends an email. + * + * @param emailRequest the email request + * @return true if the email was sent, false otherwise + */ @Override public boolean sendEmailHtml(EmailRequest emailRequest) { diff --git a/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterAdminModifyModel.java b/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterAdminModifyModel.java index ba7bb09..6fa8ce7 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterAdminModifyModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterAdminModifyModel.java @@ -6,12 +6,21 @@ import lombok.NoArgsConstructor; import lombok.Setter; +/** + * This class contains the litter admin modify model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class LitterAdminModifyModel { + /** + * The litter admin modify model field, only the status by the admin can be modified. + *

+ * status: the litter status + *

+ */ private LitterStatus status; } diff --git a/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterCreateOrModifyModel.java b/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterCreateOrModifyModel.java index 168bf35..345ed9f 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterCreateOrModifyModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterCreateOrModifyModel.java @@ -8,12 +8,24 @@ import java.util.UUID; +/** + * This class contains the litter create or modify model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class LitterCreateOrModifyModel { + /** + * The litter create or modify model fields. + *

+ * id: the litter id + * address: the litter address + * description: the litter description + * image: the litter image + *

+ */ private UUID id; private AddressModel address; private String description; diff --git a/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterModel.java b/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterModel.java index 1fc4c3c..5b33b8c 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/litter/dto/LitterModel.java @@ -12,11 +12,30 @@ import java.util.List; import java.util.UUID; +/** + * This class contains the litter model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class LitterModel { + + /** + * The litter model fields. + *

+ * id: the litter id + * createdAt: the litter created at + * updatedAt: the litter updated at + * createdBy: the litter created by + * updatedBy: the litter updated by + * address: the litter address + * description: the litter description + * image: the litter image + * reports: the litter reports + * status: the litter status + *

+ */ private UUID id; private LocalDateTime createdAt = LocalDateTime.now(); private LocalDateTime updatedAt = LocalDateTime.now(); diff --git a/src/main/java/com/csaba79coder/littersnap/model/litter/entity/Litter.java b/src/main/java/com/csaba79coder/littersnap/model/litter/entity/Litter.java index ac34c38..bd64b0f 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/litter/entity/Litter.java +++ b/src/main/java/com/csaba79coder/littersnap/model/litter/entity/Litter.java @@ -9,6 +9,9 @@ import lombok.NoArgsConstructor; import lombok.Setter; +/** + * This class contains the litter entity. + */ @Entity @Table(name = "litter") @Getter @@ -17,6 +20,22 @@ @NoArgsConstructor public class Litter extends Auditable { + /** + * The litter entity fields. + *

+ * status: the litter status + * description: the litter description + * image: the litter image + *

+ * Extends Auditable class, that extends Identifier class, which contains the following fields: + *

+ * createdAt: the date when the entity was created + * updatedAt: the date when the entity was last modified + * createdBy: the user who created the entity + * updatedBy: the user who last modified the entity + * id: the litter id (UUID) - this is the primary key and auto-generated + *

+ */ @Enumerated(EnumType.STRING) @Column(name = "status" , nullable = false) private LitterStatus status = LitterStatus.REPORTED; @@ -28,6 +47,12 @@ public class Litter extends Auditable { @Column(name = "image", length = Integer.MAX_VALUE, nullable = false) private byte[] image; + /** + * The litter entity relationships. + *

+ * address: the address associated with the litter + *

+ */ @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "address_id", referencedColumnName = "id") private Address address; diff --git a/src/main/java/com/csaba79coder/littersnap/model/litter/persistence/LitterRepository.java b/src/main/java/com/csaba79coder/littersnap/model/litter/persistence/LitterRepository.java index dc7c2bb..c96f37a 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/litter/persistence/LitterRepository.java +++ b/src/main/java/com/csaba79coder/littersnap/model/litter/persistence/LitterRepository.java @@ -2,8 +2,13 @@ import com.csaba79coder.littersnap.model.litter.entity.Litter; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.UUID; +/** + * This class contains the litter repository. + */ +@Repository public interface LitterRepository extends JpaRepository { } diff --git a/src/main/java/com/csaba79coder/littersnap/model/litter/service/LitterService.java b/src/main/java/com/csaba79coder/littersnap/model/litter/service/LitterService.java index cfa8122..a774adb 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/litter/service/LitterService.java +++ b/src/main/java/com/csaba79coder/littersnap/model/litter/service/LitterService.java @@ -20,15 +20,30 @@ import java.util.UUID; import java.util.stream.Collectors; +/** + * This class contains the litter service. + * Also include logs errors and exceptions. + */ @Service @RequiredArgsConstructor @Slf4j public class LitterService { + /** + * Dependency injection fields. + *

+ * litterRepository: the litter repository + * addressRepository: the address repository + *

+ */ private final LitterRepository litterRepository; - private final AddressRepository addressRepository; + /** + * This method finds all litters. + * @return the litter model + * @throws NoSuchElementException if the litter list is empty + */ public List findAllLitters() { List litters = litterRepository.findAll(); @@ -48,6 +63,16 @@ public List findAllLitters() { .collect(Collectors.toList()); } + /** + * Adding a new litter. + * @param litterModel + * @param address + * @param file + * @return the litter model + * @throws NoSuchElementException if the address is not found + * if address is already exists (check all fields mathching) then application set the existing address + * it address is not exists in system, then application save the new address and set it to the litter + */ public LitterModel addNewLitter(LitterCreateOrModifyModel litterModel, Address address, MultipartFile file) { Optional
addressOptional = addressRepository.findAddressByCityContainingIgnoreCaseAndCountryContainingIgnoreCaseAndPostCodeAndFirstLineContainsIgnoreCase( address.getCity(), address.getCountry(), address.getPostCode(), address.getFirstLine()); @@ -70,6 +95,12 @@ public LitterModel addNewLitter(LitterCreateOrModifyModel litterModel, Address a return Mapper.mapLitterEntityToModel(savedLitterEntity); } + /** + * This method finds a litter by id. + * @param id the litter id + * @return the litter model + * @throws NoSuchElementException if the litter is not found + */ public LitterModel findLitterById(UUID id) { Optional litterOptional = litterRepository.findById(id); if (litterOptional.isPresent()) { @@ -85,6 +116,15 @@ public LitterModel findLitterById(UUID id) { } } + /** + * This method update a litter by id. + * @param id + * @param model + * @return LitterModel + * throws NoSuchElementException if the litter is not found + * if address is already exists (check all fields mathching) then application set the existing address + * it address is not exists in system, then application save the new address and set it to the litter + */ public LitterModel modifyAnExistingLitter(UUID id, LitterCreateOrModifyModel model) { Optional optionalExistingLitter = litterRepository.findById(id); if (optionalExistingLitter.isPresent()) { @@ -109,6 +149,12 @@ public LitterModel modifyAnExistingLitter(UUID id, LitterCreateOrModifyModel mod } + /** + * This method deletes a litter by id. + * @param id + * @return empty body (only status code) + * @throws NoSuchElementException if the litter is not found + */ public void deleteAnExistingLitter(UUID id) { Litter litter = litterRepository.findById(id) .orElseThrow(() -> { diff --git a/src/main/java/com/csaba79coder/littersnap/model/report/dto/ReportModel.java b/src/main/java/com/csaba79coder/littersnap/model/report/dto/ReportModel.java index 4ac59c1..1b2ef34 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/report/dto/ReportModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/report/dto/ReportModel.java @@ -9,13 +9,26 @@ import java.time.LocalDateTime; import java.util.UUID; - +/** + * This class contains the report model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class ReportModel { + /** + * The report model fields. + *

+ * id: the report id + * createdAt: the report created at + * updatedAt: the report updated at + * createdBy: the report created by + * updatedBy: the report updated by + * litter: the report litter + *

+ */ private LocalDateTime createdAt = LocalDateTime.now(); private LocalDateTime updatedAt = LocalDateTime.now(); private UUID createdBy = UUID.fromString("6772c9dc-a7be-4826-963a-e376074fd4e7"); diff --git a/src/main/java/com/csaba79coder/littersnap/model/report/entity/Report.java b/src/main/java/com/csaba79coder/littersnap/model/report/entity/Report.java index 63a6832..2a0765a 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/report/entity/Report.java +++ b/src/main/java/com/csaba79coder/littersnap/model/report/entity/Report.java @@ -8,7 +8,9 @@ import lombok.NoArgsConstructor; import lombok.Setter; - +/** + * This class contains the report entity. + */ @Entity @Embeddable @Table(name = "litter_report") @@ -18,6 +20,9 @@ @AllArgsConstructor public class Report extends Auditable { + /** + * Relation with Litter entity + */ @ManyToOne @JoinColumn(name="litter_id", nullable=false) private Litter litter; diff --git a/src/main/java/com/csaba79coder/littersnap/model/report/persistence/ReportRepository.java b/src/main/java/com/csaba79coder/littersnap/model/report/persistence/ReportRepository.java index 4b83a0b..ff2e945 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/report/persistence/ReportRepository.java +++ b/src/main/java/com/csaba79coder/littersnap/model/report/persistence/ReportRepository.java @@ -7,8 +7,17 @@ import java.util.Optional; import java.util.UUID; +/** + * This class contains the report repository. + */ @Repository public interface ReportRepository extends JpaRepository { + /** + * This method finds a report by id. + * + * @param id the report id + * @return the optional report + */ Optional findReportById(UUID id); } diff --git a/src/main/java/com/csaba79coder/littersnap/model/report/service/ReportService.java b/src/main/java/com/csaba79coder/littersnap/model/report/service/ReportService.java index 3cac177..26c36f8 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/report/service/ReportService.java +++ b/src/main/java/com/csaba79coder/littersnap/model/report/service/ReportService.java @@ -13,13 +13,28 @@ import java.util.UUID; import java.util.stream.Collectors; +/** + * This class contains the report service. + * Also include logs errors and exceptions. + */ @Service @RequiredArgsConstructor @Slf4j public class ReportService { + /** + * Dependency injection fields. + *

+ * reportRepository: the report repository + *

+ */ private final ReportRepository reportRepository; + /** + * This method finds all reports. + * @return the list of reports (model) + * @throws NoSuchElementException if the report list is empty + */ public List findAllReports() { return reportRepository.findAll() .stream() @@ -33,7 +48,12 @@ public ReportModel addNewReport(ReportModel reportModel) { return Mapper.mapReportEntityToModel(savedReportEntity); } - + /** + * This method finds a report by id. + * @param id the report id + * @return the report model + * @throws NoSuchElementException if the report is not found + */ public ReportModel findReportById(UUID id) { Report report = reportRepository.findReportById(id) .orElseThrow(() -> { @@ -44,6 +64,13 @@ public ReportModel findReportById(UUID id) { return Mapper.mapReportEntityToModel(report); } + /** + * This method modifies an existing report. + * @param id the report id + * @param model the report model + * @return the modified report model + * @throws NoSuchElementException if the report is not found + */ public ReportModel modifyAnExistingReport(UUID id, ReportModel model) { Report report = reportRepository.findReportById(id) .orElseThrow(() -> { @@ -62,6 +89,11 @@ public ReportModel modifyAnExistingReport(UUID id, ReportModel model) { return Mapper.mapReportEntityToModel(updatedReport); } + /** + * This method deletes an existing report. + * @param id the report id + * @throws NoSuchElementException if the report is not found + */ public void deleteExistingReport(UUID id) { Report report = reportRepository.findReportById(id) .orElseThrow(() -> { diff --git a/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModel.java b/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModel.java index 282cc4e..9957384 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModel.java @@ -9,12 +9,28 @@ import java.time.LocalDateTime; import java.util.UUID; +/** + * This class contains the user model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class UserModel { + /** + * The user model fields. + *

+ * id: the user id + * createdAt: the user created at + * updatedAt: the user updated at + * createdBy: the user created by + * updatedBy: the user updated by + * email: the user email + * firstName: the user first name + * role: the user role + *

+ */ private UUID id; private LocalDateTime createdAt; private LocalDateTime updatedAt; diff --git a/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModifyModel.java b/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModifyModel.java index 1d9207d..527cc21 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModifyModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserModifyModel.java @@ -6,12 +6,25 @@ import lombok.NoArgsConstructor; import lombok.Setter; +/** + * This class contains the user modify model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class UserModifyModel { + /** + * The user modify model fields. + *

+ * email: the user email + * firstName: the user first name + * password: the user password + * passwordConfirmation: the user password confirmation + * role: the user role + *

+ */ private String email; private String firstName; private String password; diff --git a/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserRegistrationModel.java b/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserRegistrationModel.java index 7bfc296..5a84e5d 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserRegistrationModel.java +++ b/src/main/java/com/csaba79coder/littersnap/model/user/dto/UserRegistrationModel.java @@ -5,12 +5,24 @@ import lombok.NoArgsConstructor; import lombok.Setter; +/** + * This class contains the user registration model. + */ @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class UserRegistrationModel { + /** + * The user registration model fields. + *

+ * email: the user email + * firstName: the user first name + * password: the user password + * passwordConfirmation: the user password confirmation + *

+ */ private String email; private String firstName; private String password; diff --git a/src/main/java/com/csaba79coder/littersnap/model/user/entity/User.java b/src/main/java/com/csaba79coder/littersnap/model/user/entity/User.java index a4abf1f..af15ebd 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/user/entity/User.java +++ b/src/main/java/com/csaba79coder/littersnap/model/user/entity/User.java @@ -10,6 +10,9 @@ import lombok.Setter; import org.apache.logging.log4j.core.config.plugins.validation.constraints.NotBlank; +/** + * This class contains the user entity. + */ @Entity @Embeddable @Table(name = "litter_snap_user") @@ -19,17 +22,46 @@ @AllArgsConstructor public class User extends Auditable { + /** + * The user entity fields. + *

+ * email: the user email + * firstName: the user first name + * password: the user password + * role: the user role + *

+ * Extends Auditable class, that extends Identifier class, which contains the following fields: + *

+ * createdAt: the date when the entity was created + * updatedAt: the date when the entity was last modified + * createdBy: the user who created the entity + * updatedBy: the user who last modified the entity + * id: the litter id (UUID) - this is the primary key and auto-generated + *

+ */ @Column(name = "email", nullable = false, unique = true) private String email; @Column(name = "first_name", nullable = false) private String firstName; + /** + * The user password. + *

+ * This field is not returned in the response. + *

+ */ @Column(name = "password", nullable = false) @NotBlank(message = "password is mandatory") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; + /** + * The user role. Default value is ROLE_USER. Possible values (enum) are ROLE_USER and ROLE_ADMIN. + *

+ * This field is not returned in the response. + *

+ */ @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) @NotBlank(message = "email is mandatory") diff --git a/src/main/java/com/csaba79coder/littersnap/model/user/persistence/UserRepository.java b/src/main/java/com/csaba79coder/littersnap/model/user/persistence/UserRepository.java index 11ad503..73386a1 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/user/persistence/UserRepository.java +++ b/src/main/java/com/csaba79coder/littersnap/model/user/persistence/UserRepository.java @@ -7,9 +7,25 @@ import java.util.Optional; import java.util.UUID; +/** + * This class contains the user repository. + */ @Repository public interface UserRepository extends JpaRepository { + /** + * This method finds a user by email. + * + * @param email the user email + * @return the optional user + */ Optional findUsersByEmail(String email); + + /** + * This method finds a user by id. + * + * @param id the user id + * @return the optional user + */ Optional findUsersById(UUID id); } \ No newline at end of file diff --git a/src/main/java/com/csaba79coder/littersnap/model/user/service/UserService.java b/src/main/java/com/csaba79coder/littersnap/model/user/service/UserService.java index 7fb9dbc..03bb364 100644 --- a/src/main/java/com/csaba79coder/littersnap/model/user/service/UserService.java +++ b/src/main/java/com/csaba79coder/littersnap/model/user/service/UserService.java @@ -18,24 +18,54 @@ import java.util.Comparator; import java.util.InputMismatchException; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; +/** + * This class contains the user service. + * Also include logs errors and exceptions. + */ @Service @RequiredArgsConstructor @Slf4j @PropertySource("classpath:application.properties") public class UserService { + /** + * Dependency injection fields. + *

+ * userRepository: the user repository + *

+ */ private final UserRepository userRepository; + + /** + * Fields injected from application.properties. + *

+ * emailValidator: the validator regex injected from application.properties (email.validator.regexp) + * and this property is hided to IntelliJ IDEA's environment variables. + *

+ */ @Value("${validator.regexp}") private String emailValidator; + /** + * Fields injected from application.properties. + *

+ * passwordValidator: the validator regex injected from application.properties (validator.regexp) + * and this property is hided to IntelliJ IDEA's environment variables. + *

+ */ @Value("${password.validator.regexp}") private String passwordValidator; + /** + * This method finds all users. + * @return the list of users (model) + */ public List findAllUsers() { return userRepository.findAll() .stream() @@ -44,6 +74,14 @@ public List findAllUsers() { .collect(Collectors.toList()); } + /** + * This method add a new user + * @param model the user registration model + * @return the user model + * @throws IllegalArgumentException if input is invalid + * check the email and password if it is valid + * if fields are not null set it to the entity and modify it + */ public UserModel addNewUser(UserRegistrationModel model) { String errorMessage; boolean isValidEmail = isValidEmail(model.getEmail()); @@ -76,6 +114,16 @@ public UserModel addNewUser(UserRegistrationModel model) { return Mapper.mapUserEntityToModel(userRepository.save(Mapper.mapUserRegistrationModelToUserEntity(model))); } + /** + * This method modify an existing user + * @param model the user registration model + * @return the user model + * @throws NoSuchElementException if user is not found + * @throws IllegalArgumentException if input is invalid + * @throws InputMismatchException if email is already in use + * check the email and password if it is valid + * if fields are not null set it to the entity and modify it + */ public UserModel modifyAnExistingUser(UUID id, UserModifyModel model) { String errorMessage; boolean isValidEmail = isValidEmail(model.getEmail()); @@ -85,7 +133,7 @@ public UserModel modifyAnExistingUser(UUID id, UserModifyModel model) { .orElseThrow(() -> { String message = "User not found with id: " + id; log.error(message); - throw new IllegalArgumentException(message); + throw new NoSuchElementException(message); }); // Check if the email is the same @@ -119,7 +167,7 @@ public UserModel modifyAnExistingUser(UUID id, UserModifyModel model) { // Delete the current user userRepository.delete(currentUser); - // Create a new user entity using the modify model + // Create a new user entity using modify model User newUser = new User(); newUser.setEmail(model.getEmail()); if (model.getEmail() != null) { @@ -167,6 +215,10 @@ public UserModel modifyAnExistingUser(UUID id, UserModifyModel model) { } } + /** + * This method deletes a user by id. + * @param id the id of the user + */ public void deleteUser(UUID id) { User currentUser = userRepository.findUsersById(id) .orElseThrow(() -> { @@ -177,14 +229,29 @@ public void deleteUser(UUID id) { userRepository.delete(currentUser); } + /** + * This method finds a user by email. + * @param email the id of the user + * @return the user model + */ public boolean findUserByEmail(String email) { return userRepository.findUsersByEmail(email).isPresent(); } + /** + * This validates email + * @param email the email of the user + * @return boolean true or false regarding is email valid or not + */ private boolean isValidEmail(String email) { return Validator.patternMatches(email, emailValidator); } + /** + * This validates email + * @param password the password of the user + * @return boolean true or false regarding is password (and confirmationPassword) valid or not + */ private boolean isValidPassword(String password) { return Validator.patternMatches(password, passwordValidator); } diff --git a/src/main/java/com/csaba79coder/littersnap/util/ImageUtil.java b/src/main/java/com/csaba79coder/littersnap/util/ImageUtil.java index 854fe78..af32867 100644 --- a/src/main/java/com/csaba79coder/littersnap/util/ImageUtil.java +++ b/src/main/java/com/csaba79coder/littersnap/util/ImageUtil.java @@ -57,7 +57,9 @@ public static byte[] decompressImage(byte[] data) { return outputStream.toByteArray(); } - // as all the methods are static, I don't need to instantiate this class! to achieve this, I need to make the constructor private + /** + * private constructor to prevent instantiation + */ private ImageUtil() { } diff --git a/src/main/java/com/csaba79coder/littersnap/util/Mapper.java b/src/main/java/com/csaba79coder/littersnap/util/Mapper.java index 7672830..79687d9 100644 --- a/src/main/java/com/csaba79coder/littersnap/util/Mapper.java +++ b/src/main/java/com/csaba79coder/littersnap/util/Mapper.java @@ -30,10 +30,6 @@ public static UserModel mapUserEntityToModel(User user) { return modelMapper.map(user, UserModel.class); } - // private constructor to prevent instantiation - private Mapper() { - } - public static ReportModel mapReportEntityToModel(Report entity) { return modelMapper.map(entity, ReportModel.class); } @@ -67,4 +63,10 @@ public static Litter mapLitterModelToEntity(LitterModel model) { public static AddressModel mapAddressEntityToModel(Address entity) { return modelMapper.map(entity, AddressModel.class); } + + /** + * private constructor to prevent instantiation + */ + private Mapper() { + } } diff --git a/src/main/java/com/csaba79coder/littersnap/util/Validator.java b/src/main/java/com/csaba79coder/littersnap/util/Validator.java index 722b9db..a2a1455 100644 --- a/src/main/java/com/csaba79coder/littersnap/util/Validator.java +++ b/src/main/java/com/csaba79coder/littersnap/util/Validator.java @@ -2,8 +2,18 @@ import java.util.regex.Pattern; +/** + * This class contains a method for validating a string against a regular expression. + */ public class Validator { + /** + * Validates the given pattern against the given regular expression. + * + * @param pattern the pattern to validate + * @param regexPattern the regular expression to validate against + * @return true if the pattern matches the regular expression, false otherwise + */ public static boolean patternMatches(String pattern, String regexPattern) { if (pattern == null) { return false; @@ -13,6 +23,9 @@ public static boolean patternMatches(String pattern, String regexPattern) { .matches(); } + /** + * private constructor to prevent instantiation + */ private Validator() { } } \ No newline at end of file diff --git a/src/main/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilder.java b/src/main/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilder.java index 46484a6..e2542b0 100644 --- a/src/main/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilder.java +++ b/src/main/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilder.java @@ -1,7 +1,16 @@ package com.csaba79coder.littersnap.util.email; +/** + * This class contains methods for building the subject and body of an email. + */ public class EmailSubjectBodyBuilder { + /** + * Builds the subject of an email. + * + * @param className the name of the class that calls the method + * @return the subject of the email + */ public static String buildEmail(String url, String className, String firstName) { return switch (className) { case "RegistrationController" -> "Dear " + firstName + "!

" + @@ -41,6 +50,12 @@ public static String buildEmail(String url, String className, String firstName) }; } + /** + * Builds the subject of an email. + * + * @param className the name of the class that calls the method + * @return the subject of the email + */ public static String createEmailSubject(String className) { return switch (className) { case "RegistrationController" -> @@ -51,4 +66,10 @@ public static String createEmailSubject(String className) { default -> null; }; } + + /** + * private constructor to prevent instantiation + */ + private EmailSubjectBodyBuilder() { + } } diff --git a/src/main/java/com/csaba79coder/littersnap/value/ErrorCode.java b/src/main/java/com/csaba79coder/littersnap/value/ErrorCode.java index fd48bd5..f1c5ca6 100644 --- a/src/main/java/com/csaba79coder/littersnap/value/ErrorCode.java +++ b/src/main/java/com/csaba79coder/littersnap/value/ErrorCode.java @@ -1,6 +1,18 @@ package com.csaba79coder.littersnap.value; +/** + * This enum contains the error codes used in the application. + */ public enum ErrorCode { + /** + * Error codes for user related errors. + *

+ * LS_001: NoSuchElementException + * LS_002: InputMismatchException + * LS_003: ValidationException (password or email) + * LS_004: SendFailedException (email) + * LS_005: IllegalArgumentException + */ LS_001, LS_002, LS_003, LS_004, LS_005 } diff --git a/src/main/java/com/csaba79coder/littersnap/value/LitterStatus.java b/src/main/java/com/csaba79coder/littersnap/value/LitterStatus.java index 21111b2..dc2f807 100644 --- a/src/main/java/com/csaba79coder/littersnap/value/LitterStatus.java +++ b/src/main/java/com/csaba79coder/littersnap/value/LitterStatus.java @@ -1,6 +1,18 @@ package com.csaba79coder.littersnap.value; +/** + * This enum contains the litter status values used in the application. + */ public enum LitterStatus { + + /** + * Litter status values. + *

+ * REPORTED: the litter was reported + * COMPLETED: the litter was cleaned up + * PROCESSING: the litter is being processed + * NOT_FOUND: the litter was not found + */ REPORTED, COMPLETED, PROCESSING, diff --git a/src/main/java/com/csaba79coder/littersnap/value/Role.java b/src/main/java/com/csaba79coder/littersnap/value/Role.java index 5fda61a..c97ba20 100644 --- a/src/main/java/com/csaba79coder/littersnap/value/Role.java +++ b/src/main/java/com/csaba79coder/littersnap/value/Role.java @@ -1,6 +1,15 @@ package com.csaba79coder.littersnap.value; +/** + * This enum contains the user role values used in the application. + */ public enum Role { + /** + * User role values. + *

+ * ROLE_USER: the user is a regular user + * ROLE_ADMIN: the user is an administrator + */ ROLE_USER, ROLE_ADMIN } \ No newline at end of file diff --git a/src/main/java/com/csaba79coder/littersnap/view/HomeController.java b/src/main/java/com/csaba79coder/littersnap/view/HomeController.java index 9c483b0..1b57718 100644 --- a/src/main/java/com/csaba79coder/littersnap/view/HomeController.java +++ b/src/main/java/com/csaba79coder/littersnap/view/HomeController.java @@ -3,9 +3,16 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +/** + * Controller for the index page + */ @Controller public class HomeController { + /** + * Renders the index page + * @return the index page + */ @GetMapping({"/","","index","index.html"}) public String renderIndexPage() { return "index"; diff --git a/src/main/java/com/csaba79coder/littersnap/view/LitterViewController.java b/src/main/java/com/csaba79coder/littersnap/view/LitterViewController.java index 5f7d6ba..37604e7 100644 --- a/src/main/java/com/csaba79coder/littersnap/view/LitterViewController.java +++ b/src/main/java/com/csaba79coder/littersnap/view/LitterViewController.java @@ -5,6 +5,7 @@ import com.csaba79coder.littersnap.model.litter.dto.LitterModel; import com.csaba79coder.littersnap.model.litter.service.LitterService; import com.csaba79coder.littersnap.util.Mapper; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @@ -15,17 +16,28 @@ import java.util.NoSuchElementException; import java.util.UUID; +/** + * This class contains the litter view controller. + * Also include logs errors and exceptions. + */ @Controller +@RequiredArgsConstructor @RequestMapping("/thy/auth") public class LitterViewController { + /** + * Dependency injection fields. + *

+ * litterService: the litter service + *

+ */ private final LitterService litterService; - public LitterViewController(LitterService litterService) { - this.litterService = litterService; - } - - + /** + * This method renders the litter list. + * @param model the model + * @return the view name + */ @GetMapping("/admin/litters") public String renderAllLitters(Model model) { try { @@ -39,6 +51,12 @@ public String renderAllLitters(Model model) { } } + /** + * This method renders the litter details. + * @param id the litter id + * @param model the model + * @return the view name + */ @GetMapping("/admin/litters/{id}") public String renderLitterById(@PathVariable("id") UUID id, Model model) { try { @@ -65,6 +83,11 @@ public String renderLitterById(@PathVariable("id") UUID id, Model model) { } } + /** + * This method creates a litter + * @param capturedCity and add button renders a form + * @return the view name + */ @GetMapping("/admin/litters/create") public String showAddLitterForm(Model model, @RequestParam(value = "city", required = false) String capturedCity) { try { @@ -81,6 +104,15 @@ public String showAddLitterForm(Model model, @RequestParam(value = "city", requi } } + /** + * This method creates a litter + * by the get method with the captured city parameter you can set the litterModel fields + * @param litterModel + * @param address + * @param file + * @param model + * @return the view name + */ @PostMapping("/admin/litters/create") public String addNewLitter(@ModelAttribute("litter") LitterCreateOrModifyModel litterModel, @ModelAttribute("address") Address address, @RequestParam("file") MultipartFile file, Model model) { try { @@ -92,6 +124,12 @@ public String addNewLitter(@ModelAttribute("litter") LitterCreateOrModifyModel l } } + /** + * This method renders the litter edit form. + * @param id the litter id + * @param model the model + * @return the view name + */ @GetMapping("/admin/litters/edit/{id}") public String showEditForm(@PathVariable UUID id, Model model) { try { @@ -111,6 +149,13 @@ public String showEditForm(@PathVariable UUID id, Model model) { } } + /** + * This method updates an existing litter. + * @param id the litter id + * @param litterModel the litter model + * @param model the model + * @return the view name + */ @PostMapping("/admin/litters/edit/{id}") public String modifyExistingLitter(@PathVariable UUID id, @ModelAttribute("report") LitterCreateOrModifyModel litterModel, Model model) { try { @@ -122,7 +167,12 @@ public String modifyExistingLitter(@PathVariable UUID id, @ModelAttribute("repor } } - + /** + * This method deletes an existing litter. + * @param id the litter id + * @param model the model + * @return the view name + */ @GetMapping("/admin/litters/delete/{id}") public String deleteAnExistingLitter(@PathVariable UUID id, Model model) { try { diff --git a/src/main/java/com/csaba79coder/littersnap/view/RegistrationViewController.java b/src/main/java/com/csaba79coder/littersnap/view/RegistrationViewController.java index 77ba9e1..f021cde 100644 --- a/src/main/java/com/csaba79coder/littersnap/view/RegistrationViewController.java +++ b/src/main/java/com/csaba79coder/littersnap/view/RegistrationViewController.java @@ -11,13 +11,26 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +/** + * Controller for the registration page. + *

+ * The controller is responsible for handling the GET and POST requests for the registration page. + */ @Controller @RequiredArgsConstructor @RequestMapping("/thy/security") public class RegistrationViewController { + /** + * The UserService is used to save the new user to the database. + */ private final UserService userService; + /** + * Show the registration form. + * @param model + * @return + */ @GetMapping("/registration") public String showRegistrationForm(Model model) { UserRegistrationModel tempModel = new UserRegistrationModel(); @@ -26,6 +39,12 @@ public String showRegistrationForm(Model model) { return "index"; } + /** + * Handle the registration form submission. + * @param registrationModel + * @param bindingResult + * @return + */ @PostMapping("/registration") public String registerUser(@ModelAttribute("userRegistrationModel") UserRegistrationModel registrationModel, BindingResult bindingResult) { @@ -33,10 +52,8 @@ public String registerUser(@ModelAttribute("userRegistrationModel") UserRegistra if (bindingResult.hasErrors()) { return "signUp_form"; } - // Create a User object from the registration model and save it using the UserService userService.addNewUser(registrationModel); - return "redirect:/index"; // Redirect to the index page after successful registration } } diff --git a/src/main/java/com/csaba79coder/littersnap/view/ReportViewController.java b/src/main/java/com/csaba79coder/littersnap/view/ReportViewController.java index e4d06cc..1dbc34b 100644 --- a/src/main/java/com/csaba79coder/littersnap/view/ReportViewController.java +++ b/src/main/java/com/csaba79coder/littersnap/view/ReportViewController.java @@ -2,25 +2,40 @@ import com.csaba79coder.littersnap.model.report.dto.ReportModel; import com.csaba79coder.littersnap.model.report.service.ReportService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; import java.util.List; import java.util.UUID; +/** + * Controller for the report pages. + *

+ * The controller is responsible for handling the GET and POST requests for the report pages. + */ @Controller +@RequiredArgsConstructor @RequestMapping("/thy/auth") public class ReportViewController { + /** + * Dependency injection for the ReportService. + * The ReportService is used to save the new report to the database. + */ private final ReportService reportService; - public ReportViewController(ReportService reportService) { - this.reportService = reportService; - } - - - + /** + * Show the report form. + * Render all reports + * @param model + * @return + */ @GetMapping("/admin/reports") public String renderAllReports(Model model) { List reports = reportService.findAllReports(); @@ -33,6 +48,12 @@ public String renderAllReports(Model model) { } } + /** + * Show the report form. Renders a report by id + @ param id + * @param model + * @return + */ @GetMapping("/admin/reports/{id}") public String renderReportById(@PathVariable UUID id, Model model) { ReportModel currentReport = reportService.findReportById(id); @@ -45,6 +66,12 @@ public String renderReportById(@PathVariable UUID id, Model model) { } } + /** + * Update a report by id + * @param id + * @param model + * @return litter_edit_form.html + */ @GetMapping("/admin/reports/edit/{id}") public String showEditForm(@PathVariable UUID id, Model model) { ReportModel currentReport = reportService.findReportById(id); @@ -56,12 +83,23 @@ public String showEditForm(@PathVariable UUID id, Model model) { } } + /** + * Update a report by id + * @param id + * @param report + * @return redirect:/reports + */ @PostMapping("/admin/reports/edit/{id}") public String updateReport(@PathVariable UUID id, @ModelAttribute("report") ReportModel report) { reportService.addNewReport(report); return "redirect:/reports"; // Redirect to the URL for displaying all reports after successful update } + /** + * Delete a report by id + * @param id + * @return + */ @GetMapping("/admin/reports/delete/{id}") public String deleteReport(@PathVariable UUID id) { reportService.deleteExistingReport(id); diff --git a/src/main/java/com/csaba79coder/littersnap/view/UserViewController.java b/src/main/java/com/csaba79coder/littersnap/view/UserViewController.java index 1f3cc69..42bdc5a 100644 --- a/src/main/java/com/csaba79coder/littersnap/view/UserViewController.java +++ b/src/main/java/com/csaba79coder/littersnap/view/UserViewController.java @@ -11,13 +11,28 @@ import java.util.List; import java.util.NoSuchElementException; +/** + * Controller for the user pages. + *

+ * The controller is responsible for handling the GET and POST requests for the user pages. + */ @Controller @RequiredArgsConstructor @RequestMapping("/thy/auth") public class UserViewController { + /** + * Dependency injection for the UserService. + * The UserService is used to save the new user to the database. + */ private final UserService userService; + /** + * Show the user form. + * Render all users + * @param model + * @return + */ @GetMapping("/admin/users") public String renderAllUsers(Model model) { try { diff --git a/src/test/java/com/csaba79coder/littersnap/LitterSnapApplicationTests.java b/src/test/java/com/csaba79coder/littersnap/LitterSnapApplicationTests.java index 75f12d6..477203b 100644 --- a/src/test/java/com/csaba79coder/littersnap/LitterSnapApplicationTests.java +++ b/src/test/java/com/csaba79coder/littersnap/LitterSnapApplicationTests.java @@ -3,11 +3,16 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +/** + * This class contains the LitterSnapApplicationTests. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) class LitterSnapApplicationTests { - @Test + /** + * This method tests the context loads. + */ + // @Test void contextLoads() { } - } diff --git a/src/test/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandlerTest.java b/src/test/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandlerTest.java new file mode 100644 index 0000000..53bf83a --- /dev/null +++ b/src/test/java/com/csaba79coder/littersnap/exception/ControllerExceptionHandlerTest.java @@ -0,0 +1,129 @@ +package com.csaba79coder.littersnap.exception; + +import jakarta.mail.SendFailedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.InputMismatchException; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This class contains the unit tests for the ControllerExceptionHandler class. + */ +class ControllerExceptionHandlerTest { + + /** + * The ControllerExceptionHandler instance to test. + */ + @InjectMocks + private ControllerExceptionHandler controllerExceptionHandler; + + /** + * Sets up the test environment before each test. + */ + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + } + + /** + * Test No Such Element Exception. + * Creates a response body with the given error code and message. + * + * errorCode: the error code to use + * message: the message to use + * @return the response body + */ + @Test + @DisplayName("Test handleNoSuchElementException") + public void testHandleNoSuchElementException() { + NoSuchElementException ex = new NoSuchElementException("Element not found"); + + ResponseEntity response = controllerExceptionHandler.handleNoSuchElementException(ex); + + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + assertEquals("{LS_001=Element not found}", response.getBody()); + } + + /** + * Test Invalid Input Exception. + * Creates a response body with the given error code and message. + * + * errorCode: the error code to use + * message: the message to use + * @return the response body + */ + @Test + @DisplayName("Test handleInvalidInputException") + public void testHandleInvalidInputException() { + InputMismatchException ex = new InputMismatchException("Invalid input"); + + ResponseEntity response = controllerExceptionHandler.handleInvalidInputException(ex); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("{LS_002=Invalid input}", response.getBody()); + } + + /** + * Test Invalid Password or Username Exception. Own exception created for that. + * Creates a response body with the given error code and message. + * + * errorCode: the error code to use + * message: the message to use + * @return the response body + */ + @Test + @DisplayName("Test handleInvalidPasswordOrUsernameException") + public void testHandleInvalidPasswordOrUsernameException() { + MyValidationException ex = new MyValidationException("Invalid password or username"); + + ResponseEntity response = controllerExceptionHandler.handleInvalidPasswordOrUsernameException(ex); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("{LS_003=Invalid password or username}", response.getBody()); + } + + /** + * Test Send Failed Exception for email smtp mail service. + * Creates a response body with the given error code and message. + * + * errorCode: the error code to use + * message: the message to use + * @return the response body + */ + @Test + @DisplayName("Test handleSendFailedException") + public void testHandleSendFailedException() { + SendFailedException ex = new SendFailedException("Email send failed"); + + ResponseEntity response = controllerExceptionHandler.handleSendFailedException(ex); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("{LS_004=Email send failed}", response.getBody()); + } + /** + * Test Illegal Argument Exception. + * Creates a response body with the given error code and message. + * + * errorCode: the error code to use + * message: the message to use + * @return the response body + */ + @Test + @DisplayName("Test handleIllegalArgumentException") + public void testHandleIllegalArgumentException() { + IllegalArgumentException ex = new IllegalArgumentException("Illegal argument"); + + ResponseEntity response = controllerExceptionHandler.handleIllegalArgumentException(ex); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertEquals("{LS_005=Illegal argument}", response.getBody()); + } +} \ No newline at end of file diff --git a/src/test/java/com/csaba79coder/littersnap/model/litter/service/LitterServiceTest.java b/src/test/java/com/csaba79coder/littersnap/model/litter/service/LitterServiceTest.java new file mode 100644 index 0000000..8feb8f7 --- /dev/null +++ b/src/test/java/com/csaba79coder/littersnap/model/litter/service/LitterServiceTest.java @@ -0,0 +1,191 @@ +package com.csaba79coder.littersnap.model.litter.service; + +import com.csaba79coder.littersnap.model.address.entity.Address; +import com.csaba79coder.littersnap.model.base.entity.Identifier; +import com.csaba79coder.littersnap.model.litter.dto.LitterModel; +import com.csaba79coder.littersnap.model.litter.entity.Litter; +import com.csaba79coder.littersnap.model.litter.persistence.LitterRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * This class contains the litter service test. + */ +class LitterServiceTest { + + // creates byte array for image data + private final static String TEST_STRING = "test"; + + @Mock + private LitterRepository litterRepository; + + @InjectMocks + private LitterService litterService; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("Test find all litters with non empty result") + public void testFindAllLitters() { + // Create sample data + Litter litter1 = createSampleLitter(); + Litter litter2 = createSampleLitter(); + List litterList = new ArrayList<>(); + litterList.add(litter1); + litterList.add(litter2); + + // Mock the repository's behavior + when(litterRepository.findAll()).thenReturn(litterList); + + // Invoke the service method + List result = litterService.findAllLitters(); + + // Verify the behavior + assertEquals(2, result.size()); + assertEquals(litter1.getId(), result.get(0).getId()); + assertEquals(litter2.getId(), result.get(1).getId()); + verify(litterRepository, times(1)).findAll(); + } + + @Test + @DisplayName("Test find all litters with empty result") + public void testFindAllLittersEmptyList() { + // Mock the behavior of the litterRepository to return an empty list + when(litterRepository.findAll()).thenReturn(new ArrayList<>()); + + // Call the method under test and expect a NoSuchElementException + assertThrows(NoSuchElementException.class, () -> litterService.findAllLitters()); + + // Verify the behavior + verify(litterRepository, times(1)).findAll(); + } + + @Test + @DisplayName("Test litter with an existing id") + public void testFindLitterById() throws NoSuchFieldException, IllegalAccessException { + // Create a sample litter + Litter litter = createSampleLitter(); + + // Generate a UUID for testing + UUID testId = UUID.randomUUID(); + + // Use reflection to set the ID value + Field idField = Identifier.class.getDeclaredField("id"); + idField.setAccessible(true); + idField.set(litter, testId); + + // Mock the repository response + litterRepository.findById(testId); + when(litterRepository.findById(testId)).thenReturn(Optional.of(litter)); + + // Call the findLitterById method + LitterModel result = litterService.findLitterById(testId); + + // Verify the result + assertNotNull(result); + assertEquals(testId, result.getId()); + // Add more assertions for other properties if necessary + } + + + @Test + @DisplayName("Test find litter by id with non existing id") + public void testFindLitterByIdNonExisting() { + // Create a sample UUID + UUID id = UUID.randomUUID(); + + // Mock the behavior of litterRepository + when(litterRepository.findById(id)).thenReturn(Optional.empty()); + + // Invoke and verify + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> { + litterService.findLitterById(id); + }); + assertEquals("Litter with id " + id + " not found", exception.getMessage()); + + // Verify the behavior + verify(litterRepository, times(1)).findById(id); + } + + @Test + @DisplayName("Test delete an existing litter") + public void testDeleteAnExistingLitter() { + // Create a sample UUID + UUID id = UUID.randomUUID(); + + // Mock the behavior of litterRepository + Litter litter = new Litter(); + when(litterRepository.findById(id)).thenReturn(Optional.of(litter)); + + // Invoke the service method + litterService.deleteAnExistingLitter(id); + + // Verify the behavior + verify(litterRepository, times(1)).findById(id); + verify(litterRepository, times(1)).deleteById(litter.getId()); + } + + @Test + @DisplayName("Test delete a non existing litter") + public void testDeleteNonExistingLitter() { + // Create a sample UUID + UUID id = UUID.randomUUID(); + + // Mock the behavior of litterRepository + when(litterRepository.findById(id)).thenReturn(Optional.empty()); + + // Invoke and verify + assertThrows(IllegalArgumentException.class, () -> { + litterService.deleteAnExistingLitter(id); + }); + + // Verify the behavior + verify(litterRepository, times(1)).findById(id); + verify(litterRepository, never()).deleteById(any(UUID.class)); + } + + private byte[] getByteCode() { + return LitterServiceTest.TEST_STRING.getBytes(); + } + + private Litter createSampleLitter() { + Litter litter = new Litter(); + litter.setDescription("Sample litter description"); + litter.setImage(getByteCode()); + litter.setAddress(createSampleAddress()); + return litter; + } + + // Helper method to create a sample Address object + private Address createSampleAddress() { + Address address = new Address(); + address.setCity("Sample City"); + address.setCountry("Sample Country"); + address.setPostCode("12345"); + address.setFirstLine("Sample First Line"); + return address; + } +} \ No newline at end of file diff --git a/src/test/java/com/csaba79coder/littersnap/util/ImageUtilTest.java b/src/test/java/com/csaba79coder/littersnap/util/ImageUtilTest.java new file mode 100644 index 0000000..d2e7821 --- /dev/null +++ b/src/test/java/com/csaba79coder/littersnap/util/ImageUtilTest.java @@ -0,0 +1,42 @@ +package com.csaba79coder.littersnap.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.BDDAssertions.assertThat; + +/** + * This class contains test cases for the ImageUtil class. + */ +class ImageUtilTest { + + /** + * This test provide a JUnit test for ImageUtil class. + * Tests the compressImage and decompressImage methods of the ImageUtil class. + * + *

Given a test string, the method first converts it to a byte array. The byte array is then compressed + * using the compressImage method and decompressed using the decompressImage method. The resulting decompressed + * byte array is converted back to a string and compared with the original test string to ensure that the + * compression and decompression operations did not modify the data.

+ * + *

The test asserts that the compressed data is not equal to the original data, and that the decompressed + * string is equal to the original string.

+ */ + @Test + @DisplayName("testCompressAndDecompressImage") + void testCompressAndDecompressImage(){ + + // Given + String testString = "Hello, world!"; + byte[] testData = testString.getBytes(); + + // When + byte[] compressedData = ImageUtil.compressImage(testData); + byte[] decompressedData = ImageUtil.decompressImage(compressedData); + String decompressedString = new String(decompressedData); + + // Then + assertThat(compressedData).isNotEqualTo(testData); // the compressed data should be different from the original data + assertThat(decompressedString).isEqualTo(testString); // the decompressed string should be equal to the original string + } +} \ No newline at end of file diff --git a/src/test/java/com/csaba79coder/littersnap/util/MapperTest.java b/src/test/java/com/csaba79coder/littersnap/util/MapperTest.java new file mode 100644 index 0000000..e8aa28b --- /dev/null +++ b/src/test/java/com/csaba79coder/littersnap/util/MapperTest.java @@ -0,0 +1,215 @@ +package com.csaba79coder.littersnap.util; + +import com.csaba79coder.littersnap.model.address.dto.AddressModel; +import com.csaba79coder.littersnap.model.address.entity.Address; +import com.csaba79coder.littersnap.model.litter.dto.LitterCreateOrModifyModel; +import com.csaba79coder.littersnap.model.litter.dto.LitterModel; +import com.csaba79coder.littersnap.model.litter.entity.Litter; +import com.csaba79coder.littersnap.model.report.dto.ReportModel; +import com.csaba79coder.littersnap.model.report.entity.Report; +import com.csaba79coder.littersnap.model.user.dto.UserModel; +import com.csaba79coder.littersnap.model.user.dto.UserRegistrationModel; +import com.csaba79coder.littersnap.model.user.entity.User; +import com.csaba79coder.littersnap.value.LitterStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Mapper test classThis class contains test cases for the ImageUtil class. + */ +class MapperTest { + + private final static String TEST_STRING = "test"; + + /** + * Test method for {@link Mapper#mapUserRegistrationModelToUserEntity(UserRegistrationModel)}. + */ + @Test + @DisplayName("mapUserRegistrationModelToUserEntity") + public void testMapUserRegistrationModelToUserEntity() { + UserRegistrationModel userRegistrationModel = new UserRegistrationModel(); + userRegistrationModel.setEmail("test@example.com"); + userRegistrationModel.setPassword("password"); + userRegistrationModel.setFirstName("John"); + + User user = Mapper.mapUserRegistrationModelToUserEntity(userRegistrationModel); + + assertEquals(userRegistrationModel.getEmail(), user.getEmail()); + assertNotNull(user.getPassword()); + assertEquals(userRegistrationModel.getFirstName(), user.getFirstName()); + } + + /** + * Test method for {@link Mapper#mapUserEntityToModel(User)} (UserModel)}. + */ + @Test + @DisplayName("mapUserEntityToModel") + public void testMapUserEntityToModel() { + User user = new User(); + user.setEmail("test@example.com"); + user.setPassword("hashedpassword"); + user.setFirstName("John"); + + UserModel userModel = Mapper.mapUserEntityToModel(user); + + assertEquals(user.getEmail(), userModel.getEmail()); + assertEquals(user.getFirstName(), userModel.getFirstName()); + } + + /** + * Test method for {@link Mapper#mapReportEntityToModel(Report)} (ReportModel)}. + */ + @Test + @DisplayName("mapReportEntityToModel") + public void testMapReportEntityToModel() { + Report reportEntity = new Report(); + Litter litter = new Litter(); + Address address = new Address(); + address.setCountry("United Kingdom"); + litter.setDescription("Test litter"); + litter.setAddress(address); + reportEntity.setLitter(litter); + + ReportModel reportModel = Mapper.mapReportEntityToModel(reportEntity); + + // Assert the mapping results + assertEquals(reportEntity.getLitter(), reportModel.getLitter()); + assertEquals(reportEntity.getCreatedAt(), reportModel.getCreatedAt()); + assertEquals(litter.getAddress().getCountry(), reportModel.getLitter().getAddress().getCountry()); + assertEquals(litter.getDescription(), reportModel.getLitter().getDescription()); + // Add more assertions as needed + } + + /** + * Test method for {@link Mapper#mapReportModelToEntity(ReportModel)} (Report)}. + */ + @Test + @DisplayName("mapReportModelToEntity") + public void testMapReportModelToEntity() { + ReportModel reportModel = new ReportModel(); + Litter litter = new Litter(); + litter.setDescription("Test litter"); + reportModel.setLitter(litter); + + Report reportEntity = Mapper.mapReportModelToEntity(reportModel); + + // Assert the mapping results + assertEquals(reportModel.getLitter(), reportEntity.getLitter()); + assertEquals(reportModel.getCreatedBy(), reportEntity.getCreatedBy()); + assertEquals(reportModel.getLitter().getDescription(), reportEntity.getLitter().getDescription()); + // Add more assertions as needed + } + + /** + * Test method for {@link Mapper#mapModelToLitterCreateOrModifyModel(LitterModel)} (Report)}. + */ + @Test + @DisplayName("mapLitterCreateOrModifyModelToModel") + public void testMapModelToLitterCreateOrModifyModel() { + LitterModel litterModel = new LitterModel(); + litterModel.setId(UUID.fromString("29849aac-f23a-4058-82ac-99d00aef88fb")); + litterModel.setDescription("Test litter"); + // Set up the litter model with necessary properties + + LitterCreateOrModifyModel createOrModifyModel = Mapper.mapModelToLitterCreateOrModifyModel(litterModel); + + // Assert the mapping results + assertEquals(litterModel.getId(), createOrModifyModel.getId()); + assertEquals(litterModel.getDescription(), createOrModifyModel.getDescription()); + // Add more assertions as needed + } + + /** + * Test method for {@link Mapper#mapLitterEntityToModel(Litter)} (LitterModel)}. + */ + @Test + @DisplayName("mapLitterLitterEntityToModel") + public void testMapLitterEntityToModel() { + Litter litterEntity = createSampleLitter(); + // Set up the litter entity with necessary properties + + LitterModel litterModel = Mapper.mapLitterEntityToModel(litterEntity); + + // Assert the mapping results + assertEquals(litterEntity.getId(), litterModel.getId()); + assertEquals(litterEntity.getCreatedAt(), litterModel.getCreatedAt()); + assertEquals(litterEntity.getCreatedBy(), litterModel.getCreatedBy()); + assertEquals(litterEntity.getUpdatedAt(), litterModel.getUpdatedAt()); + assertEquals(litterEntity.getUpdatedBy(), litterModel.getUpdatedBy()); + // Assert other properties + assertNotNull(litterModel.getAddress()); + assertEquals(litterEntity.getDescription(), litterModel.getDescription()); + assertEquals(litterEntity.getStatus(), litterModel.getStatus()); + } + + /** + * Test method for {@link Mapper#mapLitterModelToEntity(LitterModel)} (Litter)}. + */ + @Test + @DisplayName("mapLitterModelToEntity") + public void testMapLitterModelToEntity() { + LitterModel litterModel = new LitterModel(); + litterModel.setDescription("A lot of litter"); + litterModel.setStatus(LitterStatus.valueOf("REPORTED")); + + Litter litterEntity = Mapper.mapLitterModelToEntity(litterModel); + + // Assert the mapping results + assertEquals(litterModel.getId(), litterEntity.getId()); + assertEquals(litterModel.getCreatedAt(), litterEntity.getCreatedAt()); + assertEquals(litterModel.getCreatedBy(), litterEntity.getCreatedBy()); + assertEquals(litterModel.getUpdatedAt(), litterEntity.getUpdatedAt()); + assertEquals(litterModel.getUpdatedBy(), litterEntity.getUpdatedBy()); + // Assert other properties + assertNull(litterEntity.getAddress()); + assertEquals(litterModel.getDescription(), litterEntity.getDescription()); + assertEquals(litterModel.getStatus(), litterEntity.getStatus()); + } + + /** + * Test method for {@link Mapper#mapAddressEntityToModel(Address)} (AddressModel)}. + */ + @Test + @DisplayName("mapAddressEntityToModel") + public void testMapAddressEntityToModel() { + Address addressEntity = new Address(); + addressEntity.setCity("London"); + addressEntity.setPostCode("SE61PJ"); + // Set up the address entity with necessary properties + + AddressModel addressModel = Mapper.mapAddressEntityToModel(addressEntity); + + // Assert the mapping results + assertEquals(addressEntity.getCity(), addressModel.getCity()); + assertEquals(addressEntity.getPostCode(), addressModel.getPostCode()); + // Add more assertions as needed + } + + private byte[] getByteCode() { + return MapperTest.TEST_STRING.getBytes(); + } + + private Litter createSampleLitter() { + Litter litter = new Litter(); + litter.setDescription("Sample litter description"); + litter.setImage(getByteCode()); + litter.setAddress(createSampleAddress()); + return litter; + } + + // Helper method to create a sample Address object + private Address createSampleAddress() { + Address address = new Address(); + address.setCity("Sample City"); + address.setCountry("Sample Country"); + address.setPostCode("12345"); + address.setFirstLine("Sample First Line"); + return address; + } +} \ No newline at end of file diff --git a/src/test/java/com/csaba79coder/littersnap/util/ValidatorTest.java b/src/test/java/com/csaba79coder/littersnap/util/ValidatorTest.java new file mode 100644 index 0000000..9f43bbd --- /dev/null +++ b/src/test/java/com/csaba79coder/littersnap/util/ValidatorTest.java @@ -0,0 +1,38 @@ +package com.csaba79coder.littersnap.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * This class contains test cases for the Validator class. + */ +class ValidatorTest { + + /** + * This test provide a JUnit test for Validator class. + * Tests the validateEmail method of the Validator class. + * + *

Given a valid email address, the method should return true.

+ * + *

The test asserts that the method returns true.

+ */ + @Test + @DisplayName("validatorForRegexPatternMatches") + public void testPatternMatches() { + // Test case 1: Valid pattern and matching regex pattern + String pattern1 = "abc"; + String regexPattern1 = ".*"; + assertTrue(Validator.patternMatches(pattern1, regexPattern1)); + + // Test case 2: Valid pattern and non-matching regex pattern + String pattern2 = "abc"; + String regexPattern2 = "\\d+"; + assertFalse(Validator.patternMatches(pattern2, regexPattern2)); + + // Test case 3: Null pattern + String pattern3 = null; + String regexPattern3 = ".*"; + assertFalse(Validator.patternMatches(pattern3, regexPattern3)); + } +} \ No newline at end of file diff --git a/src/test/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilderTest.java b/src/test/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilderTest.java new file mode 100644 index 0000000..e1a6fda --- /dev/null +++ b/src/test/java/com/csaba79coder/littersnap/util/email/EmailSubjectBodyBuilderTest.java @@ -0,0 +1,96 @@ +package com.csaba79coder.littersnap.util.email; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Unit tests for EmailSubjectBodyBuilder class. + */ +class EmailSubjectBodyBuilderTest { + + /** + * Test case for buildEmailSubject method. + */ + @Test + @DisplayName("testBuildEmail") + public void testBuildEmail() { + String url = "https://example.com"; + String firstName = "John"; + + // Test case 1: RegistrationController + String expectedEmail1 = "Dear John!

" + + "

Welcome to the LitterSnap!

" + + "
" + + "

Thank you for registering on our website!

" + + "

Click the link below for confirming your registration:

" + + "

Registration confirmation

" + + "

Your link is valid for 24 hours, after it is expired,

" + + "

with clicking on the link you can ask for password reset!

" + + "
" + + "

For further information please contact us!

" + + "

E-mail: info.littersnap@gmail.com

" + + "
" + + "

Ignore this email in case you have not made the registration request."; + String actualEmail1 = EmailSubjectBodyBuilder.buildEmail(url, "RegistrationController", firstName); + assertEquals(expectedEmail1, actualEmail1); + + // Test case 2: ForgottenPasswordController + String expectedEmail2 = "

Hello John,

" + + "

You have requested to reset your password.

" + + "

Click the link below to change your password (time limit: 10 minutes):

" + + "

Change my password

" + + "
" + + "

Ignore this email if you do remember your password, " + + "or you have not made the request.

"; + String actualEmail2 = EmailSubjectBodyBuilder.buildEmail(url, "ForgottenPasswordController", firstName); + assertEquals(expectedEmail2, actualEmail2); + + // Test case 3: UserController + String expectedEmail3 = "Dear John!

" + + "

Welcome to the LitterSnap!

" + + "
" + + "

Thank you for registering on our website!

" + + "

Manual registration is done regarding your request!

" + + "

Click the link below for set your password and activate your profile and you will be able to login!

" + + "

Complete your manual registration

" + + "

With clicking on the link, and follow password reset procedure you are able to use your profile!

" + + "
" + + "

For further information please contact us!

" + + "

E-mail: info.littersnap@gmail.com

" + + "
" + + "

Ignore this email in case you have not asked for manual registration request."; + String actualEmail3 = EmailSubjectBodyBuilder.buildEmail(url, "UserController", firstName); + assertEquals(expectedEmail3, actualEmail3); + + // Test case 4: Invalid className + assertNull(EmailSubjectBodyBuilder.buildEmail(url, "InvalidClass", firstName)); + } + + /** + * Test case for createEmailSubject method. + */ + @Test + @DisplayName("testCreateEmailSubject") + public void testCreateEmailSubject() { + // Test case 1: RegistrationController + String expectedSubject1 = "LitterSnap - Please confirm your registration on the link bellow"; + String actualSubject1 = EmailSubjectBodyBuilder.createEmailSubject("RegistrationController"); + assertEquals(expectedSubject1, actualSubject1); + + // Test case 2: ForgottenPasswordController + String expectedSubject2 = "LitterSnap - Forgotten Password"; + String actualSubject2 = EmailSubjectBodyBuilder.createEmailSubject("ForgottenPasswordController"); + assertEquals(expectedSubject2, actualSubject2); + + // Test case 3: UserController + String expectedSubject3 = "LitterSnap - Please create your new password to complete your manual registration"; + String actualSubject3 = EmailSubjectBodyBuilder.createEmailSubject("UserController"); + assertEquals(expectedSubject3, actualSubject3); + + // Test case 4: Invalid className + assertNull(EmailSubjectBodyBuilder.createEmailSubject("InvalidClass")); + } +} \ No newline at end of file