diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3c74e2ca5a5a6e2286f6fc6c425b11db1b698b24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# General +/bak + +# Eclipse +.project +.classpath +.settings +WebContent + +# Maven +/bin +/target +/assembly + +# Testing +/servers +C:\\nppdf32Log\\debuglog.txt + +# Angular +src/main/angular diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ef7e7efc09c9d471c391f05b9567966928085840 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/cd b/cd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/db/mysql-with-inserts.sql b/db/mysql-with-inserts.sql new file mode 100644 index 0000000000000000000000000000000000000000..10b99501213f277bcf5bc45af00eea3e7edbd506 --- /dev/null +++ b/db/mysql-with-inserts.sql @@ -0,0 +1,46 @@ +DROP DATABASE IF EXISTS `daaexample`; +CREATE DATABASE `daaexample`; + +CREATE TABLE `daaexample`.`people` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `surname` varchar(100) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `daaexample`.`users` ( + `login` varchar(100) NOT NULL, + `password` varchar(64) NOT NULL, + `role` varchar(10) NOT NULL, + PRIMARY KEY (`login`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `daaexample`.`pets` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `food` varchar(100) NOT NULL, + `id_person` int NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `id_person` FOREIGN KEY (`id`) REFERENCES `daaexample`.`people`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE USER IF NOT EXISTS 'daa'@'localhost' IDENTIFIED WITH mysql_native_password BY 'daa'; +GRANT ALL ON `daaexample`.* TO 'daa'@'localhost'; + +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Antón','Pérez'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Manuel','Martínez'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Laura','Reboredo'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Perico','Palotes'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Ana','María'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'María','Nuevo'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Alba','Fernández'); +INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Asunción','Jiménez'); + +INSERT INTO `daaexample`.`pets` (`id`,`name`,`food`,`id_person`) VALUES (0,'Ali','pienso',0); +INSERT INTO `daaexample`.`pets` (`id`,`name`,`food`,`id_person`) VALUES (0,'Pepe','atún',0); + +-- The password for each user is its login suffixed with "pass". For example, user "admin" has the password "adminpass". +INSERT INTO `daaexample`.`users` (`login`,`password`,`role`) +VALUES ('admin', '713bfda78870bf9d1b261f565286f85e97ee614efe5f0faf7c34e7ca4f65baca','ADMIN'); +INSERT INTO `daaexample`.`users` (`login`,`password`,`role`) +VALUES ('normal', '7bf24d6ca2242430343ab7e3efb89559a47784eea1123be989c1b2fb2ef66e83','USER'); diff --git a/db/mysql.sql b/db/mysql.sql new file mode 100644 index 0000000000000000000000000000000000000000..9334caa740d9e1ca8427065bd8f35dec6616676a --- /dev/null +++ b/db/mysql.sql @@ -0,0 +1,28 @@ +DROP DATABASE IF EXISTS `daaexample`; +CREATE DATABASE `daaexample`; + +CREATE TABLE `daaexample`.`people` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `surname` varchar(100) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `daaexample`.`users` ( + `login` varchar(100) NOT NULL, + `password` varchar(64) NOT NULL, + `role` varchar(10) NOT NULL, + PRIMARY KEY (`login`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `daaexample`.`pets` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `food` varchar(100) NOT NULL, + `id_person` int NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `id_person` FOREIGN KEY (`id`) REFERENCES `daaexample`.`people`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE USER IF NOT EXISTS 'daa'@'localhost' IDENTIFIED WITH mysql_native_password BY 'daa'; +GRANT ALL ON `daaexample`.* TO 'daa'@'localhost'; diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..76c19487b6d890323fe4695454b3735d9a0fe910 --- /dev/null +++ b/pom.xml @@ -0,0 +1,423 @@ + + 4.0.0 + es.uvigo.esei.daa + example + war + 0.1.19 + DAA Example + + + + GNU GENERAL PUBLIC LICENSE, Version 3 + http://www.gnu.org/licenses/gpl.html + repo + + + + + + 1.8 + 1.8 + UTF-8 + UTF-8 + ${project.basedir}/servers + false + true + 8.5.27 + 6300 + + + 3.1.0 + 2.25 + 2.2.0 + 1.7.21 + + + 4.13.2 + 2.0.0.0 + 3.5.1 + 3.141.59 + 4.3.14.RELEASE + 2.5.1 + 1.3.0 + 2.3.3 + 5.1.45 + 2.4.2 + + + 2.20.1 + 3.0.0 + 2.20.1 + 2.20.1 + 3.2.0 + 0.8.0 + 1.6.6 + 1.0.0 + 1.8 + 1.0.6 + + + + + javax.servlet + javax.servlet-api + ${java.servlet.version} + provided + + + + org.glassfish.jersey.containers + jersey-container-servlet + ${jersey.version} + + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + + + + org.slf4j + slf4j-jdk14 + ${slf4j-jdk14.version} + + + + + junit + junit + ${junit.version} + test + + + + org.hamcrest + java-hamcrest + ${java-hamcrest.version} + + + + org.easymock + easymock + ${easymock.version} + test + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${jersey.version} + test + + + + org.apache.commons + commons-dbcp2 + ${commons-dbcp2.version} + test + + + + org.seleniumhq.selenium + selenium-java + ${selenium-java.version} + test + + + + org.springframework + spring-test + ${spring-test.version} + test + + + + org.springframework + spring-context + ${spring-test.version} + test + + + + org.springframework + spring-jdbc + ${spring-test.version} + test + + + + org.dbunit + dbunit + ${dbunit.version} + jar + test + + + + com.github.springtestdbunit + spring-test-dbunit + ${spring-test-dbunit.version} + test + + + + org.hsqldb + hsqldb + ${hsqldb.version} + test + + + + mysql + mysql-connector-java + ${mysql.version} + test + + + + nl.jqno.equalsverifier + equalsverifier + ${equalsverifier.version} + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + ${maven-jxr-plugin.version} + + + + + + DAAExample + + + + maven-war-plugin + org.apache.maven.plugins + ${maven-war-plugin.version} + + ${project.finalName} + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/UnitTestSuite.java + + + ${geckodriver.uncompressed.path} + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + **/IntegrationTestSuite.java + + + + + default-integration-tests + + integration-test + verify + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + ${maven-surefire-report-plugin.version} + + + + test-report + test + + report-only + + + + integration-test-report + integration-test + + report-only + failsafe-report-only + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + ${jacoco.port} + + + + default-prepare-agent + + prepare-agent + + + + coverage-report + test + + report + + + + default-check + + check + + + + jacoco-report + post-integration-test + + report + + + ${project.reporting.outputDirectory}/jacoco-it + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.apache.maven.plugins + + + maven-antrun-plugin + + + [1.8,) + + + run + + + + + + + + + + + + + + + + + run + + false + + + + + + com.fizzed + fizzed-watcher-maven-plugin + ${fizzed-watcher-maven-plugin.version} + + true + + + src/main + + + + package + cargo:redeploy + + + + + + org.codehaus.cargo + cargo-maven2-plugin + ${cargo-maven2-plugin.version} + + + tomcat8x + + https://repo1.maven.org/maven2/org/apache/tomcat/tomcat/${tomcat.version}/tomcat-${tomcat.version}.zip + ${project.servers.directory}/downloads + ${project.servers.directory}/extracts + + + + mysql + mysql-connector-java + + + + + + ${project.build.directory}/catalina-base + + + tomcat/server.mysql.xml + conf/server.xml + + + + 9080 + + cargo.datasource.jndi=jdbc/daaexample| + cargo.datasource.driver=com.mysql.jdbc.Driver| + cargo.datasource.url=jdbc:mysql://localhost/daaexample?useSSL=false| + cargo.datasource.username=daa| + cargo.datasource.password=daa| + cargo.datasource.maxActive=8| + cargo.datasource.maxIdle=4| + cargo.datasource.maxWait=10000 + + + + + + + + + + diff --git a/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java b/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..295943a08661510366fdaeefca042842d2418986 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java @@ -0,0 +1,43 @@ +package es.uvigo.esei.daa; + +import static java.util.stream.Collectors.toSet; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +import es.uvigo.esei.daa.rest.PeopleResource; +import es.uvigo.esei.daa.rest.UsersResource; +import es.uvigo.esei.daa.rest.PetsResource; + +/** + * Configuration of the REST application. This class includes the resources and + * configuration parameter used in the REST API of the application. + * + * @author Miguel Reboiro Jato + * + *///aplicaciones con varios recursos asociados. internamente se sirve aplìcation + +@ApplicationPath("/rest/*") +public class DAAExampleApplication extends Application {//todos recursos llevan /rest/ + @Override + public Set> getClasses() {//se sobrescribe conjunto java con clases que quiero servir + return Stream.of( + PeopleResource.class, + UsersResource.class, + PetsResource.class + ).collect(toSet()); + } + + @Override + public Map getProperties() { + // Activates JSON automatic conversion in JAX-RS + return Collections.singletonMap( + "com.sun.jersey.api.json.POJOMappingFeature", true + );//cuando respuesta es java la pasa a json + } +} diff --git a/src/main/java/es/uvigo/esei/daa/dao/DAO.java b/src/main/java/es/uvigo/esei/daa/dao/DAO.java new file mode 100644 index 0000000000000000000000000000000000000000..2610b0714475ea6f9fbbbda4765d3ed6a14453c2 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/dao/DAO.java @@ -0,0 +1,48 @@ +package es.uvigo.esei.daa.dao; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +//permite pedir cosas a tomcat +/** + * Simple base class for DAO (Data Access Object) classes. This super-class is + * responsible for providing a {@link java.sql.Connection} to its sub-classes. + * + * @author Miguel Reboiro Jato + * + */ +public abstract class DAO { + private final static Logger LOG = Logger.getLogger(DAO.class.getName()); + private final static String JNDI_NAME = "java:/comp/env/jdbc/daaexample"; + + private DataSource dataSource; + + /** + * Constructs a new instance of {@link DAO}. + */ + public DAO() { + try { + this.dataSource = (DataSource) new InitialContext().lookup(JNDI_NAME);//obtiene fuente datos(DataSource) + } catch (NamingException e) { + LOG.log(Level.SEVERE, "Error initializing DAO", e); + throw new RuntimeException(e); + } + } + + /** + * Returns an open {@link java.sql.Connection}. + * + * @return an open {@link java.sql.Connection}. + * @throws SQLException if an error happens while establishing the + * connection with the database. + */ + protected Connection getConnection() throws SQLException { + return this.dataSource.getConnection(); + } +} diff --git a/src/main/java/es/uvigo/esei/daa/dao/DAOException.java b/src/main/java/es/uvigo/esei/daa/dao/DAOException.java new file mode 100644 index 0000000000000000000000000000000000000000..ffd4233f07cfcbbed73c264c235efd0317b9ffcf --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/dao/DAOException.java @@ -0,0 +1,83 @@ +package es.uvigo.esei.daa.dao; + +/** + * A general exception class for the DAO layer. + * + * @author Miguel Reboiro Jato + */ +public class DAOException extends Exception { + private static final long serialVersionUID = 1L; + + /** + * Constructs a new instance of {@link DAOException} with {@code null} as + * its detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + */ + public DAOException() { + } + + /** + * Constructs a new instance of {@link DAOException} with the specified + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for later + * retrieval by the {@link #getMessage()} method. + */ + public DAOException(String message) { + super(message); + } + + /** + * Constructs a new instance of {@link DAOException} with the specified + * cause and a detail message of + * {@code (cause==null ? null : cause.toString())} (which typically contains + * the class and detail message of {@code cause}). This constructor is + * useful for exceptions that are little more than wrappers for other + * throwables (for example, {@link java.security.PrivilegedActionException}). + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is permitted, and + * indicates that the cause is nonexistent or unknown.) + */ + public DAOException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new instance of {@link DAOException} with the specified + * detail message and cause. + * + *

Note that the detail message associated with {@code cause} is + * not automatically incorporated in this exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). A {@code null} value is permitted, and + * indicates that the cause is nonexistent or unknown. + */ + public DAOException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new instance of {@link DAOException} with the specified + * detail message, cause, suppression enabled or disabled, and writable + * stack trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. A {@code null} value is permitted, and indicates + * that the cause is nonexistent or unknown. + * @param enableSuppression whether or not suppression is enabled or + * disabled. + * @param writableStackTrace whether or not the stack trace should be + * writable. + */ + public DAOException(String message, Throwable cause, + boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java b/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java new file mode 100644 index 0000000000000000000000000000000000000000..f4565b437855497784f5c5121a959cfd6f926d42 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java @@ -0,0 +1,193 @@ +package es.uvigo.esei.daa.dao; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import es.uvigo.esei.daa.entities.Person; + +/** + * DAO class for the {@link Person} entities. + * + * @author Miguel Reboiro Jato + * + */ +public class PeopleDAO extends DAO { + private final static Logger LOG = Logger.getLogger(PeopleDAO.class.getName()); + + /** + * Returns a person stored persisted in the system. + * + * @param id identifier of the person. + * @return a person with the provided identifier. + * @throws DAOException if an error happens while retrieving the person. + * @throws IllegalArgumentException if the provided id does not corresponds + * with any persisted person. + */ + //JBBC + public Person get(int id) + throws DAOException, IllegalArgumentException {//Igual que en users + try (final Connection conn = this.getConnection()) {//consegimos coneción bd + final String query = "SELECT * FROM people WHERE id=?";//creamos consulta + + try (final PreparedStatement statement = conn.prepareStatement(query)) {//conseguimos statement + + statement.setInt(1, id);//doy valor a ?- ls 1º valor 1 + //ejecuto consulta que devuelve result set- y conjunto resultado nos da acceso a resultados(iterando) + try (final ResultSet result = statement.executeQuery()) { + if (result.next()) { + return rowToEntity(result); + } else { + throw new IllegalArgumentException("Invalid id"); + } + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error getting a person", e); + throw new DAOException(e); + } + } + + /** + * Returns a list with all the people persisted in the system. + * + * @return a list with all the people persisted in the system. + * @throws DAOException if an error happens while retrieving the people. + */ + public List list() throws DAOException {//todas las personas + try (final Connection conn = this.getConnection()) { + final String query = "SELECT * FROM people"; + + try (final PreparedStatement statement = conn.prepareStatement(query)) { + try (final ResultSet result = statement.executeQuery()) {//manejo ResultSet + final List people = new LinkedList<>(); + + while (result.next()) {//mientras haya resultados se añadiran + people.add(rowToEntity(result));//dame valor de ese campo, creando persona + } + + return people; + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error listing people", e); + throw new DAOException(e); + } + } + + /** + * Persists a new person in the system. An identifier will be assigned + * automatically to the new person. + * + * @param name name of the new person. Can't be {@code null}. + * @param surname surname of the new person. Can't be {@code null}. + * @return a {@link Person} entity representing the persisted person. + * @throws DAOException if an error happens while persisting the new person. + * @throws IllegalArgumentException if the name or surname are {@code null}. + */ + public Person add(String name, String surname) + throws DAOException, IllegalArgumentException {//añadir persona + if (name == null || surname == null) { + throw new IllegalArgumentException("name and surname can't be null"); //no se puede insertar vacio + } + + try (Connection conn = this.getConnection()) {//si se puede conectar con BD + final String query = "INSERT INTO people VALUES(null, ?, ?)"; //se realiza insercion + + try (PreparedStatement statement = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, name); + statement.setString(2, surname); + + if (statement.executeUpdate() == 1) { + try (ResultSet resultKeys = statement.getGeneratedKeys()) { + if (resultKeys.next()) { + return new Person(resultKeys.getInt(1), name, surname); + } else { + LOG.log(Level.SEVERE, "Error retrieving inserted id"); + throw new SQLException("Error retrieving inserted id"); + } + } + } else { + LOG.log(Level.SEVERE, "Error inserting value"); + throw new SQLException("Error inserting value"); + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error adding a person", e); + throw new DAOException(e); + } + } + + /** + * Modifies a person previously persisted in the system. The person will be + * retrieved by the provided id and its current name and surname will be + * replaced with the provided. + * + * @param person a {@link Person} entity with the new data. + * @throws DAOException if an error happens while modifying the new person. + * @throws IllegalArgumentException if the person is {@code null}. + */ + public void modify(Person person) + throws DAOException, IllegalArgumentException { + if (person == null) { + throw new IllegalArgumentException("person can't be null"); + } + + try (Connection conn = this.getConnection()) { + final String query = "UPDATE people SET name=?, surname=? WHERE id=?"; + + try (PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString(1, person.getName()); + statement.setString(2, person.getSurname()); + statement.setInt(3, person.getId()); + + if (statement.executeUpdate() != 1) { + throw new IllegalArgumentException("name and surname can't be null"); + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error modifying a person", e); + throw new DAOException(); + } + } + + /** + * Removes a persisted person from the system. + * + * @param id identifier of the person to be deleted. + * @throws DAOException if an error happens while deleting the person. + * @throws IllegalArgumentException if the provided id does not corresponds + * with any persisted person. + */ + public void delete(int id) + throws DAOException, IllegalArgumentException { + try (final Connection conn = this.getConnection()) { + final String query = "DELETE FROM people WHERE id=?"; + + try (final PreparedStatement statement = conn.prepareStatement(query)) { + statement.setInt(1, id); + + if (statement.executeUpdate() != 1) { + throw new IllegalArgumentException("Invalid id"); + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error deleting a person", e); + throw new DAOException(e); + } + } + + private Person rowToEntity(ResultSet row) throws SQLException { + return new Person( + row.getInt("id"), + row.getString("name"), + row.getString("surname") + ); + } +} diff --git a/src/main/java/es/uvigo/esei/daa/dao/PetsDAO.java b/src/main/java/es/uvigo/esei/daa/dao/PetsDAO.java new file mode 100644 index 0000000000000000000000000000000000000000..fe51b0199feffb27dcf0a9f1ec4f664e009995fe --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/dao/PetsDAO.java @@ -0,0 +1,202 @@ +package es.uvigo.esei.daa.dao; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import es.uvigo.esei.daa.entities.Pet; + +/** + * DAO class for the {@link Pet} entities. + * + * @author Iria Martínez Álvarez + * + */ +public class PetsDAO extends DAO { + private final static Logger LOG = Logger.getLogger(PetsDAO.class.getName()); + + /** + * Returns a pet stored persisted in the system. + * + * @param id identifier of the pet. + * @param id_person identifier of the pet. + * @return a pet with the provided identifier. + * @throws DAOException if an error happens while retrieving the pet. + * @throws IllegalArgumentException if the provided id does not corresponds + * with any persisted pet. + */ + public Pet get(int id) + throws DAOException, IllegalArgumentException {//Igual que en users + try (final Connection conn = this.getConnection()) { + + final String query = "SELECT * FROM pets WHERE id=?"; + + try (final PreparedStatement statement = conn.prepareStatement(query)) { + statement.setInt(1, id); + + try (final ResultSet result = statement.executeQuery()) { + if (result.next()) { + return rowToEntity(result); + } else { + throw new IllegalArgumentException("Invalid id"); + } + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error getting a pet", e); + throw new DAOException(e); + } + } + + /** + * Returns a list with all the pets persisted in the system. + * + * @return a list with all the pets persisted in the system. + * @throws DAOException if an error happens while retrieving the pets. + */ + public List listPets(int id_person) throws DAOException {// todas las pets + + try (final Connection conn = this.getConnection()) { + final String query = "SELECT * FROM pets WHERE id_person=?"; + try (final PreparedStatement statement = conn.prepareStatement(query)) { + statement.setInt(1, id_person); + + try (final ResultSet result = statement.executeQuery()) { + final List pets = new LinkedList<>(); + + while (result.next()) { + pets.add(rowToEntity(result)); + } + + return pets; + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error listing pets", e); + throw new DAOException(e); + } + } + + /** + * Persists a new pet in the system. An identifier will be assigned + * automatically to the new pet. + * + * @param name name of the new pet. Can't be {@code null}. + * @param food food of the new pet. Can't be {@code null}. + * @param id_person id_person of the new pet. Can't be {@code null}. + * @return a {@link Pet} entity representing the persisted pet. + * @throws DAOException if an error happens while persisting the new pet. + * @throws IllegalArgumentException if the name or food are {@code null}. + */ + public Pet add(String name, String food, int id_person) + throws DAOException, IllegalArgumentException {// añadir pet + if (name == null || food == null) { + throw new IllegalArgumentException("name and food can't be null"); // no se puede insertar vacio + } + + try (Connection conn = this.getConnection()) {// si se puede conectar con BD + System.err.println(name + " - " + food + " - " + id_person); + final String query = "INSERT INTO pets (name, food, id_person) VALUES (?, ?, ?)"; // se realiza + // insercion + + try (PreparedStatement statement = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, name); + statement.setString(2, food); + statement.setInt(3, id_person); + + if (statement.executeUpdate() == 1) { + try (ResultSet resultKeys = statement.getGeneratedKeys()) { + if (resultKeys.next()) { + return new Pet(resultKeys.getInt(1), name, food, id_person); + } else { + LOG.log(Level.SEVERE, "Error retrieving inserted id"); + throw new SQLException("Error retrieving inserted id"); + } + } + } else { + LOG.log(Level.SEVERE, "Error inserting value"); + throw new SQLException("Error inserting value"); + } + } + + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error adding a pet", e); + throw new DAOException(e); + } + } + + /** + * Modifies a pet previously persisted in the system. The pet will be + * retrieved by the provided id and its current name,id_person and food will be + * replaced with the provided. + * + * @param pet a {@link Pet} entity with the new data. + * @throws DAOException if an error happens while modifying the new pet. + * @throws IllegalArgumentException if the pet is {@code null}. + */ + public void modify(Pet pet) + throws DAOException, IllegalArgumentException { + if (pet == null) { + throw new IllegalArgumentException("pet can't be null"); + } + + try (Connection conn = this.getConnection()) { + final String query = "UPDATE pets SET name=?, food=?, id_person=? WHERE id=?"; + + try (PreparedStatement statement = conn.prepareStatement(query)) { + statement.setString(1, pet.getName()); + statement.setString(2, pet.getFood()); + statement.setInt(3, pet.getId_person()); + statement.setInt(4, pet.getId()); + + if (statement.executeUpdate() != 1) { + throw new IllegalArgumentException("name,id_person and food can't be null"); + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error modifying a pet", e); + throw new DAOException(); + } + } + + /** + * Removes a persisted pet from the system. + * + * @param id identifier of the pet to be deleted. + * @throws DAOException if an error happens while deleting the pet. + * @throws IllegalArgumentException if the provided id does not corresponds + * with any persisted pet. + */ + public void delete(int id) + throws DAOException, IllegalArgumentException { + try (final Connection conn = this.getConnection()) { + final String query = "DELETE FROM pets WHERE id=?"; + + try (final PreparedStatement statement = conn.prepareStatement(query)) { + statement.setInt(1, id); + + if (statement.executeUpdate() != 1) { + throw new IllegalArgumentException("Invalid id"); + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error deleting a pet", e); + throw new DAOException(e); + } + } + + private Pet rowToEntity(ResultSet row) throws SQLException { + return new Pet( + row.getInt("id"), + row.getString("name"), + row.getString("food"), + row.getInt("id_person") + ); + } +} diff --git a/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java b/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java new file mode 100644 index 0000000000000000000000000000000000000000..603718e7ee2173e59e8d92b08b3ad205fd8aa6c3 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java @@ -0,0 +1,108 @@ +package es.uvigo.esei.daa.dao; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import es.uvigo.esei.daa.entities.User; + +/** + * DAO class for managing the users of the system. + * + * @author Miguel Reboiro Jato + */ +public class UsersDAO extends DAO { + private final static Logger LOG = Logger.getLogger(UsersDAO.class.getName()); + + /** + * Returns a user stored persisted in the system. + * + * @param login the login of the user to be retrieved. + * @return a user with the provided login. + * @throws DAOException if an error happens while retrieving the user. + * @throws IllegalArgumentException if the provided login does not + * corresponds with any persisted user. + */ + public User get(String login) throws DAOException { //obtiene usuario + try (final Connection conn = this.getConnection()) {//prueba a establecer conexión con BD + //se establece conexión + final String query = "SELECT * FROM users WHERE login=?";//busco que coincida el login + + try (final PreparedStatement statement = conn.prepareStatement(query)) {//secuencias que se puede ejecutar varias veces pero con distintos parametros + statement.setString(1, login); + + try (final ResultSet result = statement.executeQuery()) { + if (result.next()) { + return rowToEntity(result); + } else { + throw new IllegalArgumentException("Invalid id"); + } + } + } + } catch (SQLException e) { + LOG.log(Level.SEVERE, "Error checking login", e); + throw new DAOException(e); + } + } + + /** + * Checks if the provided credentials (login and password) correspond with a + * valid user registered in the system. + * + *

The password is stored in the system "salted" and encoded with the + * SHA-256 algorithm.

+ * + * @param login the login of the user. + * @param password the password of the user. + * @return {@code true} if the credentials are valid. {@code false} + * otherwise. + * @throws DAOException if an error happens while checking the credentials. + */ + public boolean checkLogin(String login, String password) throws DAOException {//comprueba credenciales + try { + final User user = this.get(login);//obtenemos login + + final String dbPassword = user.getPassword();//obtenemos contraseña + final String shaPassword = encodeSha256(password);//descodificamos + + return shaPassword.equals(dbPassword);//miramos que coincida + } catch (IllegalArgumentException iae) { + return false; + } + } + + private final static String encodeSha256(String text) {//descodificamos + try { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final byte[] digested = digest.digest(text.getBytes()); + + return hexToString(digested); + } catch (NoSuchAlgorithmException e) { + LOG.log(Level.SEVERE, "SHA-256 not supported", e); + throw new RuntimeException(e); + } + } + + private final static String hexToString(byte[] hex) {//parte descodificación + final StringBuilder sb = new StringBuilder(); + + for (byte b : hex) { + sb.append(String.format("%02x", b & 0xff)); + } + + return sb.toString(); + } + + private User rowToEntity(ResultSet result) throws SQLException {//devuelve usuario + return new User( + result.getString("login"), + result.getString("password"), + result.getString("role") + ); + } +} diff --git a/src/main/java/es/uvigo/esei/daa/entities/Person.java b/src/main/java/es/uvigo/esei/daa/entities/Person.java new file mode 100644 index 0000000000000000000000000000000000000000..b2f3285a5263ba6f4316d1dfa1eadfd45ae0b2d8 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/entities/Person.java @@ -0,0 +1,99 @@ +package es.uvigo.esei.daa.entities; + +import static java.util.Objects.requireNonNull; + +/** + * An entity that represents a person. + * + * @author Miguel Reboiro Jato + */ +public class Person { + private int id; + private String name; + private String surname; + + // Constructor needed for the JSON conversion + Person() {} + + /** + * Constructs a new instance of {@link Person}. + * + * @param id identifier of the person. + * @param name name of the person. + * @param surname surname of the person. + */ + public Person(int id, String name, String surname) { + this.id = id; + this.setName(name); + this.setSurname(surname); + } + + /** + * Returns the identifier of the person. + * + * @return the identifier of the person. + */ + public int getId() { + return id; + } + + /** + * Returns the name of the person. + * + * @return the name of the person. + */ + public String getName() { + return name; + } + + /** + * Set the name of this person. + * + * @param name the new name of the person. + * @throws NullPointerException if the {@code name} is {@code null}. + */ + public void setName(String name) { + this.name = requireNonNull(name, "Name can't be null"); + } + + /** + * Returns the surname of the person. + * + * @return the surname of the person. + */ + public String getSurname() { + return surname; + } + + /** + * Set the surname of this person. + * + * @param surname the new surname of the person. + * @throws NullPointerException if the {@code surname} is {@code null}. + */ + public void setSurname(String surname) { + this.surname = requireNonNull(surname, "Surname can't be null"); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Person)) + return false; + Person other = (Person) obj; + if (id != other.id) + return false; + return true; + } +} diff --git a/src/main/java/es/uvigo/esei/daa/entities/Pet.java b/src/main/java/es/uvigo/esei/daa/entities/Pet.java new file mode 100644 index 0000000000000000000000000000000000000000..961613bbc6b396198930d5883ab4864e15f02e59 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/entities/Pet.java @@ -0,0 +1,122 @@ +package es.uvigo.esei.daa.entities; + +import static java.util.Objects.requireNonNull; + +/** + * An entity that represents a pet. + * + * @author Iria Martínez Álvarez + */ +public class Pet { + private int id; + private String name; + private String food; + private int id_person; + + + // Constructor needed for the JSON conversion + Pet() {} + + /** + * Constructs a new instance of {@link Pet}. + * + * @param id identifier of the pet. + * @param name name of the pet. + * @param food food of the pet. + * @param id_person id_person of the pet. + */ + public Pet(int id, String name, String food, int id_person) { + this.id = id; + this.setName(name); + this.setFood(food); + this.setId_person(id_person); + + } + + /** + * Returns the identifier of the pet. + * + * @return the identifier of the pet. + */ + public int getId() { + return id; + } + + /** + * Returns the name of the pet. + * + * @return the name of the pet. + */ + public String getName() { + return name; + } + + /** + * Set the name of this pet. + * + * @param name the new name of the pet. + * @throws NullPointerException if the {@code name} is {@code null}. + */ + public void setName(String name) { + this.name = requireNonNull(name, "Name can't be null"); + } + + /** + * Returns the food of the pet. + * + * @return the food of the pet. + */ + public String getFood() { + return food; + } + + /** + * Set the food of this pet. + * + * @param food the new food of the pet. + * @throws NullPointerException if the {@code food} is {@code null}. + */ + public void setFood(String food) { + this.food = requireNonNull(food, "Food can't be null"); + } + /** + * Returns the id_person of the pet. + * + * @return the id_person of the pet. + */ + public int getId_person() { + return id_person; + } + + /** + * Set the id_person of this pet. + * + * @param id_person the new id_person of the pet. + * @throws NullPointerException if the {@code id_person} is {@code id_person}. + */ + public void setId_person(int id_person) { + this.id_person = requireNonNull(id_person, "Id_person can't be null"); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Pet)) + return false; + Pet other = (Pet) obj; + if (id != other.id) + return false; + return true; + } +} diff --git a/src/main/java/es/uvigo/esei/daa/entities/User.java b/src/main/java/es/uvigo/esei/daa/entities/User.java new file mode 100644 index 0000000000000000000000000000000000000000..cb13b8e8c4e42b57595e75222994ac88c40c6210 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/entities/User.java @@ -0,0 +1,88 @@ +package es.uvigo.esei.daa.entities; + +import static java.util.Objects.requireNonNull; + +/** + * An entity that represents a user. + * + * @author Miguel Reboiro Jato + */ +public class User { + private String login; + private String password; + private String role; + + // Constructor needed for the JSON conversion + User() {} + + /** + * Constructs a new instance of {@link User}. + * + * @param login login that identifies the user in the system. + * @param password password of the user encoded using SHA-256 and with the + * "salt" prefix added. + */ + public User(String login, String password, String role) { + this.setLogin(login); + this.setPassword(password); + this.setRole(role); + } + + /** + * Returns the login of the user. + * + * @return the login of the user. + */ + public String getLogin() { + return login; + } + + /** + * Sets the login of the user. + * + * @param login the login that identifies the user in the system. + */ + public void setLogin(String login) { + this.login = requireNonNull(login, "Login can't be null"); + } + + /** + * Returns the password of the user. + * + * @return the password of the user. + */ + public String getPassword() { + return password; + } + + /** + * Sets the users password. + * @param password the password of the user encoded using SHA-256 and with + * the "salt" prefix added. + */ + public void setPassword(String password) { + requireNonNull(password, "Password can't be null"); + if (!password.matches("[a-zA-Z0-9]{64}")) + throw new IllegalArgumentException("Password must be a valid SHA-256"); + + this.password = password; + } + + /** + * Returns the role of the user. + * + * @return the role of the user. + */ + public String getRole() { + return role; + } + + /** + * Sets the role of the user. + * + * @param role the role of the user + */ + public void setRole(String role) { + this.role = requireNonNull(role, "Role can't be null"); + } +} diff --git a/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java b/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java new file mode 100644 index 0000000000000000000000000000000000000000..29d0dcdfb0518b2d1cb4088d4918449eb73adf01 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java @@ -0,0 +1,213 @@ +package es.uvigo.esei.daa.rest; + +//Especificación de java para trabajar con rest + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import es.uvigo.esei.daa.dao.DAOException; +import es.uvigo.esei.daa.dao.PeopleDAO; +import es.uvigo.esei.daa.entities.Person; + +/** + * REST resource for managing people. + * + * @author Miguel Reboiro Jato. + */ +@Path("/people") +@Produces(MediaType.APPLICATION_JSON) +public class PeopleResource { + private final static Logger LOG = Logger.getLogger(PeopleResource.class.getName()); + + private final PeopleDAO dao; + + /** + * Constructs a new instance of {@link PeopleResource}. + */ + public PeopleResource() { + this(new PeopleDAO()); + } + + // Needed for testing purposes + PeopleResource(PeopleDAO dao) { + this.dao = dao; + } + + /** + * Returns a person with the provided identifier. + * + * @param id the identifier of the person to retrieve. + * @return a 200 OK response with a person that has the provided identifier. + * If the identifier does not corresponds with any user, a 400 Bad Request + * response with an error message will be returned. If an error happens + * while retrieving the list, a 500 Internal Server Error response with an + * error message will be returned. + */ + @GET + @Path("/{id}") + public Response get( + @PathParam("id") int id + ) {//response genera respuestas http de forma sencilla + try { + final Person person = this.dao.get(id);//entidades + + return Response.ok(person).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid person id in get method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error getting a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Returns the complete list of people stored in the system. + * + * @return a 200 OK response with the complete list of people stored in the + * system. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ + @GET + public Response list() { + try { + return Response.ok(this.dao.list()).build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error listing people", e); + return Response.serverError().entity(e.getMessage()).build(); + } + } + + /** + * Creates a new person in the system. + * + * @param name the name of the new person. + * @param surname the surname of the new person. + * @return a 200 OK response with a person that has been created. If the + * name or the surname are not provided, a 400 Bad Request response with an + * error message will be returned. If an error happens while retrieving the + * list, a 500 Internal Server Error response with an error message will be + * returned. + */ + @POST + public Response add( + @FormParam("name") String name, + @FormParam("surname") String surname + ) { + try { + final Person newPerson = this.dao.add(name, surname); + + return Response.ok(newPerson).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid person id in add method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error adding a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Modifies the data of a person. + * + * @param id identifier of the person to modify. + * @param name the new name of the person. + * @param surname the new surname of the person. + * @return a 200 OK response with a person that has been modified. If the + * identifier does not corresponds with any user or the name or surname are + * not provided, a 400 Bad Request response with an error message will be + * returned. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ + @PUT + @Path("/{id}") + public Response modify( + @PathParam("id") int id, + @FormParam("name") String name, + @FormParam("surname") String surname + ) { + try { + final Person modifiedPerson = new Person(id, name, surname); + this.dao.modify(modifiedPerson); + + return Response.ok(modifiedPerson).build(); + } catch (NullPointerException npe) { + final String message = String.format("Invalid data for person (name: %s, surname: %s)", name, surname); + + LOG.log(Level.FINE, message); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(message) + .build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid person id in modify method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error modifying a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Deletes a person from the system. + * + * @param id the identifier of the person to be deleted. + * @return a 200 OK response with the identifier of the person that has + * been deleted. If the identifier does not corresponds with any user, a 400 + * Bad Request response with an error message will be returned. If an error + * happens while retrieving the list, a 500 Internal Server Error response + * with an error message will be returned. + */ + @DELETE + @Path("/{id}") + public Response delete( + @PathParam("id") int id + ) { + try { + this.dao.delete(id); + + return Response.ok(id).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid person id in delete method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error deleting a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } +} diff --git a/src/main/java/es/uvigo/esei/daa/rest/PetsResource.java b/src/main/java/es/uvigo/esei/daa/rest/PetsResource.java new file mode 100644 index 0000000000000000000000000000000000000000..ee975542f676393be1392fc00844b1929fe887df --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/rest/PetsResource.java @@ -0,0 +1,230 @@ +package es.uvigo.esei.daa.rest; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import es.uvigo.esei.daa.dao.DAOException; +import es.uvigo.esei.daa.dao.PetsDAO; +import es.uvigo.esei.daa.entities.Pet; + +/** + * REST resource for managing pet. + * + * @author Iria Martínez Álvarez. + */ +@Path("/pets") +@Produces(MediaType.APPLICATION_JSON) +public class PetsResource { + private final static Logger LOG = Logger.getLogger(PetsResource.class.getName()); + + private final PetsDAO dao; + + /** + * Constructs a new instance of {@link PetsResource}. + */ + public PetsResource() { + this(new PetsDAO()); + } + + // Needed for testing purposes + PetsResource(PetsDAO dao) { + this.dao = dao; + } + + /** + * Returns a pet with the provided identifier. + * + * @param id the identifier of the pet to retrieve. + * @return a 200 OK response with a pet that has the provided identifier. + * If the identifier does not corresponds with any user, a 400 Bad Request + * response with an error message will be returned. If an error happens + * while retrieving the list, a 500 Internal Server Error response with an + * error message will be returned. + */ + @GET + @Path("/{id}") + public Response get( + @PathParam("id") int id + ) { + try { + final Pet pet = this.dao.get(id); + + return Response.ok(pet).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid pet id in get method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error getting a pet", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Returns the complete list of pets stored in the system. + * + * @return a 200 OK response with the complete list of pets stored in the + * system. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ + @GET + public Response listPets( + @QueryParam("id_person") int id_person + ) { + try { + return Response.ok(this.dao.listPets(id_person)).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid id_person", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error listing pets", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Creates a new pet in the system. + * + * @param name the name of the new pet. + * @param food the food of the new pet. + * @param id_person the id_person of the new pet. + + * @return a 200 OK response with a pet that has been created. If the + * name or the food are not provided, a 400 Bad Request response with an + * error message will be returned. If an error happens while retrieving the + * list, a 500 Internal Server Error response with an error message will be + * returned. + */ + @POST + public Response add( + @FormParam("name") String name, + @FormParam("food") String food, + @FormParam("id_person") int id_person + + ) { + try { + final Pet newPet = this.dao.add(name, food, id_person); + + return Response.ok(newPet).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid pet id in add method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error adding a pet", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Modifies the data of a pet. + * + * @param id identifier of the pet to modify. + * @param name the new name of the pet. + * @param food the new food of the pet. + * @param id_person the new id_person of the pet. + * @return a 200 OK response with a pet that has been modified. If the + * identifier does not corresponds with any user or the name,id_person or food are + * not provided, a 400 Bad Request response with an error message will be + * returned. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ + @PUT + @Path("/{id}") + public Response modify( + @PathParam("id") int id, + @FormParam("name") String name, + @FormParam("food") String food, + @FormParam("id_person") int id_person + + ) { + try { + final Pet modifiedPet = new Pet(id, name, food, id_person); + this.dao.modify(modifiedPet); + + return Response.ok(modifiedPet).build(); + } catch (NullPointerException npe) { + final String message = String.format("Invalid data for peet (name: %s, food: %s, id_person: %d)", name, food); + + LOG.log(Level.FINE, message); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(message) + .build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid pet id in modify method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error modifying a pet", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Deletes a pet from the system. + * + * @param id the identifier of the pet to be deleted. + * @return a 200 OK response with the identifier of the pet that has + * been deleted. If the identifier does not corresponds with any user, a 400 + * Bad Request response with an error message will be returned. If an error + * happens while retrieving the list, a 500 Internal Server Error response + * with an error message will be returned. + */ + @DELETE + @Path("/{id}") + public Response delete( + @PathParam("id") int id + ) { + try { + this.dao.delete(id); + + return Response.ok(id).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid pet id in delete method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error deleting a pet", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } +} diff --git a/src/main/java/es/uvigo/esei/daa/rest/UsersResource.java b/src/main/java/es/uvigo/esei/daa/rest/UsersResource.java new file mode 100644 index 0000000000000000000000000000000000000000..858c52c566cb156e62e80cff75a9961d0c56d100 --- /dev/null +++ b/src/main/java/es/uvigo/esei/daa/rest/UsersResource.java @@ -0,0 +1,100 @@ +package es.uvigo.esei.daa.rest; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +import es.uvigo.esei.daa.dao.DAOException; +import es.uvigo.esei.daa.dao.UsersDAO; +/** + * REST resource for managing users. + * + * @author Miguel Reboiro Jato. + */ +@Path("/users") +@Produces(MediaType.APPLICATION_JSON) +public class UsersResource { + private final static Logger LOG = Logger.getLogger(UsersResource.class.getName()); + + private final UsersDAO dao; + + private @Context SecurityContext security; + + /** + * Constructs a new instance of {@link UsersResource}. + */ + public UsersResource() { + this(new UsersDAO()); + } + + // Needed for testing purposes + UsersResource(UsersDAO dao) { + this(dao, null); + } + + // Needed for testing purposes + UsersResource(UsersDAO dao, SecurityContext security) { + this.dao = dao; + this.security = security; + } + + /** + * Returns a user with the provided login. + * + * @param login the identifier of the user to retrieve. + * @return a 200 OK response with an user that has the provided login. + * If the request is done without providing the login credentials or using + * invalid credentials a 401 Unauthorized response will be returned. If the + * credentials are provided and a regular user (i.e. non admin user) tries + * to access the data of other user, a 403 Forbidden response will be + * returned. If the credentials are OK, but the login does not corresponds + * with any user, a 400 Bad Request response with an error message will be + * returned. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ + @GET + @Path("/{login}") + public Response get( + @PathParam("login") String login + ) { + final String loggedUser = getLogin(); + + // Each user can only access his or her own data. Only the admin user + // can access the data of any user. + if (loggedUser.equals(login) || this.isAdmin()) { + try { + return Response.ok(dao.get(login)).build(); + } catch (IllegalArgumentException iae) { + LOG.log(Level.FINE, "Invalid user login in get method", iae); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(iae.getMessage()) + .build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error getting an user", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } else { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + } + + private String getLogin() { + return this.security.getUserPrincipal().getName(); + } + + private boolean isAdmin() { + return this.security.isUserInRole("ADMIN"); + } +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000000000000000000000000000000000..728bac7c922301c09c77ce79e6cb7ae7c640d81f --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,89 @@ + + + DAAExample + + + index.html + + + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + * + + + cors.allowed.headers + Authorization + + + cors.allowed.methods + GET, POST, DELETE, PUT + + + + CorsFilter + /rest/* + + + + + Protected Area + /rest/* + PUT + DELETE + GET + POST + + + ADMIN + USER + + + + + + Admin Area + /rest/people/* + GET + PUT + DELETE + POST + + + ADMIN + + + + + + Admin Area + /rest/pets/* + GET + PUT + DELETE + POST + + + ADMIN + + + + + + ADMIN + + + USER + + + + BASIC + DAAExample + + \ No newline at end of file diff --git a/src/main/webapp/css/login.css b/src/main/webapp/css/login.css new file mode 100644 index 0000000000000000000000000000000000000000..d6b2a480a9bd014c3943ba4687b3f7bf43da3ee5 --- /dev/null +++ b/src/main/webapp/css/login.css @@ -0,0 +1,48 @@ +html, body { + height: 100%; +} + +body { + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-align: center; + -ms-flex-pack: center; + -webkit-box-align: center; + align-items: center; + -webkit-box-pack: center; + justify-content: center; + padding-top: 40px; + padding-bottom: 40px; +} + +#form-signin { + width: 100%; + max-width: 330px; + padding: 15px; + margin: 0 auto; +} + +#form-signin .form-control { + position: relative; + box-sizing: border-box; + height: auto; + padding: 10px; + font-size: 16px; +} + +#form-signin .form-control:focus { + z-index: 2; +} + +#login { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +#password { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} \ No newline at end of file diff --git a/src/main/webapp/css/main.css b/src/main/webapp/css/main.css new file mode 100644 index 0000000000000000000000000000000000000000..a2fb74a0d513d6baaa2479df1f7b259cb784feeb --- /dev/null +++ b/src/main/webapp/css/main.css @@ -0,0 +1,3 @@ +button,input{ + margin-top: 10px; +} diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 0000000000000000000000000000000000000000..095822992f63e7069860e253b35ffe70bdb51902 --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,45 @@ + + + + + + +DAA Example - Login + + + + + +
+

DAA Example

+ + + + + +
+ + + + + + + \ No newline at end of file diff --git a/src/main/webapp/js/dao/people.js b/src/main/webapp/js/dao/people.js new file mode 100644 index 0000000000000000000000000000000000000000..6c0c26462958c834e12f4da178ca25344fa5aba7 --- /dev/null +++ b/src/main/webapp/js/dao/people.js @@ -0,0 +1,52 @@ +var PeopleDAO = (function() { + var resourcePath = "rest/people/"; + var requestByAjax = function(data, done, fail, always) { + done = typeof done !== 'undefined' ? done : function() {}; + fail = typeof fail !== 'undefined' ? fail : function() {}; + always = typeof always !== 'undefined' ? always : function() {}; + + let authToken = localStorage.getItem('authorization-token'); + if (authToken !== null) { + data.beforeSend = function(xhr) { + xhr.setRequestHeader('Authorization', 'Basic ' + authToken); + }; + } + + $.ajax(data).done(done).fail(fail).always(always); + }; + + function PeopleDAO() { + this.listPeople = function(done, fail, always) { + requestByAjax({ + url : resourcePath, + type : 'GET' + }, done, fail, always); + }; + + this.addPerson = function(person, done, fail, always) { + requestByAjax({ + url : resourcePath, + type : 'POST', + data : person + }, done, fail, always); + }; + + this.modifyPerson = function(person, done, fail, always) { + requestByAjax({ + url : resourcePath + person.id, + type : 'PUT', + data : person + }, done, fail, always); + }; + + this.deletePerson = function(id, done, fail, always) { + requestByAjax({ + url : resourcePath + id, + type : 'DELETE', + }, done, fail, always); + }; + + } + + return PeopleDAO; +})(); \ No newline at end of file diff --git a/src/main/webapp/js/dao/pets.js b/src/main/webapp/js/dao/pets.js new file mode 100644 index 0000000000000000000000000000000000000000..f7c5eda6d7a7aeba4a6a966b1a900e9fe2696649 --- /dev/null +++ b/src/main/webapp/js/dao/pets.js @@ -0,0 +1,53 @@ + var PetsDAO = (function() { + var resourcePath = "rest/pets/"; + var requestByAjax = function(data, done, fail, always) { + done = typeof done !== 'undefined' ? done : function() {}; + fail = typeof fail !== 'undefined' ? fail : function() {}; + always = typeof always !== 'undefined' ? always : function() {}; + + let authToken = localStorage.getItem('authorization-token'); + if (authToken !== null) { + data.beforeSend = function(xhr) { + xhr.setRequestHeader('Authorization', 'Basic ' + authToken); + }; + } + + $.ajax(data).done(done).fail(fail).always(always); + }; + + function PetsDAO() { + this.listPets = function(id_person,done, fail, always) { + requestByAjax({ + url : resourcePath + '?id_person=' + id_person, + type : 'GET' + }, done, fail, always); + }; + + this.addPet = function(pet, done, fail, always) { + requestByAjax({ + url : resourcePath , + type : 'POST', + data : pet + }, done, fail, always); + }; + + this.modifyPet = function(pet, done, fail, always) { + requestByAjax({ + url : resourcePath + pet.id, + type : 'PUT', + data : pet + }, done, fail, always); + }; + + this.deletePet = function(id, done, fail, always) { + requestByAjax({ + url : resourcePath + id, + type : 'DELETE', + }, done, fail, always); + }; + + + } + + return PetsDAO; +})(); \ No newline at end of file diff --git a/src/main/webapp/js/login.js b/src/main/webapp/js/login.js new file mode 100644 index 0000000000000000000000000000000000000000..585c1dd56307fddd688b1c31dcca474c8be76672 --- /dev/null +++ b/src/main/webapp/js/login.js @@ -0,0 +1,21 @@ +function doLogin(login, password) { + $.ajax({ + url: 'rest/users/' + login, + type: 'GET', + beforeSend: function (xhr) { + xhr.setRequestHeader('Authorization', 'Basic ' + btoa(login + ":" + password)); + } + }) + .done(function() { + localStorage.setItem('authorization-token', btoa(login + ":" + password)); + window.location = 'main.html'; + }) + .fail(function() { + alert('Invalid login and/or password.'); + }); +} + +function doLogout() { + localStorage.removeItem('authorization-token'); + window.location = 'index.html'; +} \ No newline at end of file diff --git a/src/main/webapp/js/view/people.js b/src/main/webapp/js/view/people.js new file mode 100644 index 0000000000000000000000000000000000000000..08bfc69be41791700be549db6741dc72027c0a7e --- /dev/null +++ b/src/main/webapp/js/view/people.js @@ -0,0 +1,213 @@ +var PeopleView = (function() { + var dao; + + // Referencia a this que permite acceder a las funciones públicas desde las funciones de jQuery. + var self; + var viewPets; + var formId = 'people-form'; + var listId = 'people-list'; + var formQuery = '#' + formId; + var listQuery = '#' + listId; + + function PeopleView(peopleDao, formContainerId, listContainerId, viewP) { + dao = peopleDao; + self = this; + viewPets=viewP + + + this.init = function() { + insertPeopleForm($('#' + formContainerId)); + insertPeopleList($('#' + listContainerId)); + + dao.listPeople(function(people) { + $.each(people, function(key, person) { + appendToTable(person); + }); + }, + function() {//falla + alert('No has sido posible acceder al listado de personas.'); + }); + + // La acción por defecto de enviar formulario (submit) se sobreescribe + // para que el envío sea a través de AJAX + $(formQuery).submit(function(event) { + var person = self.getPersonInForm(); + + if (self.isEditing()) { + dao.modifyPerson(person, + function(person) { + $('#person-' + person.id + ' td.name').text(person.name); + $('#person-' + person.id + ' td.surname').text(person.surname); + self.resetForm(); + }, + showErrorMessage, + self.enableForm + ); + } else { + dao.addPerson(person, + function(person) { + appendToTable(person); + self.resetForm(); + }, + showErrorMessage, + self.enableForm + ); + } + + return false; + }); + + $('#btnClear').click(this.resetForm); + }; + + this.getPersonInForm = function() { + var form = $(formQuery); + return { + 'id': form.find('input[name="id"]').val(), + 'name': form.find('input[name="name"]').val(), + 'surname': form.find('input[name="surname"]').val() + }; + }; + + this.getPersonInRow = function(id) { + var row = $('#person-' + id); + + if (row !== undefined) { + return { + 'id': id, + 'name': row.find('td.name').text(), + 'surname': row.find('td.surname').text() + }; + } else { + return undefined; + } + }; + + this.editPerson = function(id) { + var row = $('#person-' + id); + + if (row !== undefined) { + var form = $(formQuery); + + form.find('input[name="id"]').val(id); + form.find('input[name="name"]').val(row.find('td.name').text()); + form.find('input[name="surname"]').val(row.find('td.surname').text()); + + $('input#btnSubmit').val('Modificar'); + } + }; + + this.deletePerson = function(id) { + if (confirm('Está a punto de eliminar a una persona. ¿Está seguro de que desea continuar?')) { + dao.deletePerson(id, + function() { + $('tr#person-' + id).remove(); + }, + showErrorMessage + ); + } + }; + + this.petPerson = function(id) { + + $('#' + formContainerId).empty(); + $('#' + listContainerId).empty(); + + viewPets.init(self,id); + }; + + this.isEditing = function() { + return $(formQuery + ' input[name="id"]').val() != ""; + }; + + this.disableForm = function() { + $(formQuery + ' input').prop('disabled', true); + }; + + this.enableForm = function() { + $(formQuery + ' input').prop('disabled', false); + }; + + this.resetForm = function() { + $(formQuery)[0].reset(); + $(formQuery + ' input[name="id"]').val(''); + $('#btnSubmit').val('Crear'); + }; + }; + + var insertPeopleList = function(parent) { + parent.append( + '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
NombreApellido 
' + ); + }; + + var insertPeopleForm = function(parent) { + parent.append( + '
\ + \ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ + \ +
\ +
\ +
' + ); + }; + + var createPersonRow = function(person) { + return '\ + ' + person.name + '\ + ' + person.surname + '\ + \ + Editar\ + Eliminar\ + Mascotas\ + \ + '; + }; + + var showErrorMessage = function(jqxhr, textStatus, error) { + alert(textStatus + ": " + error); + }; + + var addRowListeners = function(person) { + $('#person-' + person.id + ' a.edit').click(function() { + self.editPerson(person.id); + }); + + $('#person-' + person.id + ' a.delete').click(function() { + self.deletePerson(person.id); + }); + + $('#person-' + person.id + ' a.pet').click(function() { + self.petPerson(person.id); + }); + + + }; + + var appendToTable = function(person) { + $(listQuery + ' > tbody:last') + .append(createPersonRow(person)); + addRowListeners(person); + }; + + return PeopleView; +})(); diff --git a/src/main/webapp/js/view/pets.js b/src/main/webapp/js/view/pets.js new file mode 100644 index 0000000000000000000000000000000000000000..adba2f0c9387472b3fdc4b73bfa1a54b7c32ac23 --- /dev/null +++ b/src/main/webapp/js/view/pets.js @@ -0,0 +1,222 @@ +var PetsView = (function() {//creo clase + var dao; + + // Referencia a this que permite acceder a las funciones públicas desde las funciones de jQuery. + //estas variables son atributos privados clase + var self; + var peopleView; + var formId = 'pets-form'; + var listId = 'pets-list'; + var formQuery = '#' + formId; + var listQuery = '#' + listId; + + function PetsView(petsDao, formContainerId, listContainerId) {//constructor + dao = petsDao;//se guarda internamente + self = this; + //metodos internas=publicas + this.init = function(peopleView,id) {//peopleView se pasa para el volver + peopleView=peopleView; + self.peopleView=peopleView; + + id_person=id; + + insertPetsForm($('#' + formContainerId));//se guarda form de pets- busco elem con ese id. Se añade html + insertPetsList($('#' + listContainerId));//se guarda lista de pets- se añade html + + dao.listPets(id_person,function(pets) {//se pide al dao que pida al backend lista de pets + $.each(pets, function(key, pet) {//para cada pet + appendToTable(pet);//se añade tabla + }); + }, + function() { + alert('No has sido posible acceder al listado de mascotas.'); + }); + + // La acción por defecto de enviar formulario (submit) se sobreescribe + // para que el envío sea a través de AJAX + $(formQuery).submit(function(event) {//se coge formulario en evento submit y se sustituye por peticion js + var pet = self.getPetInForm(); + + if (self.isEditing()) {//si se esta editando + dao.modifyPet(pet, + function(pet) { + $('#pet-' + pet.id + ' td.name').text(pet.name); + $('#pet-' + pet.id + ' td.food').text(pet.food); + self.resetForm(); + }, + showErrorMessage, + self.enableForm + ); + } else { + dao.addPet(pet, + function(pet) {//si se esta añadiendo + appendToTable(pet,peopleView);//añade tabla + self.resetForm(); + }, + showErrorMessage, + self.enableForm + ); + } + + return false; + }); + + $('#btnClear').click(this.resetForm); + $('#btnReturn').click(this.returnPeople); + }; + + this.getPetInForm = function() { + var form = $(formQuery); + return { + 'id': form.find('input[name="id"]').val(), + 'name': form.find('input[name="name"]').val(), + 'food': form.find('input[name="food"]').val(), + 'id_person': form.find('input[name="id_person"]').val() + + }; + }; + + this.getPetInRow = function(id) { + var row = $('#pet-' + id); + + if (row !== undefined) { + return { + 'id': id, + 'name': row.find('td.name').text(), + 'food': row.find('td.food').text(), + 'id_person': row.find('td.id_person').text() + + }; + } else { + return undefined; + } + }; + + this.editPet = function(id) { + var row = $('#pet-' + id); + + if (row !== undefined) { + var form = $(formQuery); + + form.find('input[name="id"]').val(id); + form.find('input[name="name"]').val(row.find('td.name').text()); + form.find('input[name="food"]').val(row.find('td.food').text()); + form.find('input[name="id_person"]').val(id_person); + + $('input#btnSubmit').val('Modificar'); + } + }; + + this.deletePet = function(id) { + if (confirm('Está a punto de eliminar a una mascota. ¿Está seguro de que desea continuar?')) { + dao.deletePet(id, + function() { + $('tr#pet-' + id).remove(); + }, + showErrorMessage + ); + } + }; + + this.isEditing = function() { + return $(formQuery + ' input[name="id"]').val() != ""; + }; + + this.disableForm = function() { + $(formQuery + ' input').prop('disabled', true); + }; + + this.enableForm = function() { + $(formQuery + ' input').prop('disabled', false); + }; + + this.resetForm = function() { + $(formQuery)[0].reset(); + $(formQuery + ' input[name="id"]').val(''); + $('#btnSubmit').val('Crear'); + }; + this.returnPeople = function() { + window.history.back(); + }; + }; + + var insertPetsList = function(parent) { + parent.append( + '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
NombreComida 
' + ); + }; + //metodos privadas + + var insertPetsForm = function(parent) { + parent.append( + '
\ +

Mascotas

\ + \ +
\ + \ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ + \ +
\ +
\ +
' + ); + }; + + var createPetsRow = function(pet) { + return '\ + ' + pet.name + '\ + ' + pet.id_person + '\ + ' + pet.food + '\ + \ + Editar\ + Eliminar\ + \ + '; + }; + + var showErrorMessage = function(jqxhr, textStatus, error) { + alert(textStatus + ": " + error); + }; + + var addRowListeners = function(pet) { + $('#pet-' + pet.id + ' a.edit').click(function() { + self.editPet(pet.id); + }); + + $('#pet-' + pet.id + ' a.delete').click(function() { + self.deletePet(pet.id); + }); + }; + + + var appendToTable = function(pet,peopleView) { + $(listQuery + ' > tbody:last')//coge la tabla, coge ultimo elem + .append(createPetsRow(pet));//se añade elem html + addRowListeners(pet);//añade listeners(botones) + + }; + + return PetsView; +})(); diff --git a/src/main/webapp/main.html b/src/main/webapp/main.html new file mode 100644 index 0000000000000000000000000000000000000000..eecff17538ea7ada85314eca140b21599b2d1583 --- /dev/null +++ b/src/main/webapp/main.html @@ -0,0 +1,57 @@ + + + + + +DAA Example + + + + +
+ +
+ +
+
+

Personas

+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/es/uvigo/esei/daa/DAAExampleTestApplication.java b/src/test/java/es/uvigo/esei/daa/DAAExampleTestApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..43b5809bf3c4fc31f47c7e7d960c7acdba0aa762 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/DAAExampleTestApplication.java @@ -0,0 +1,22 @@ +package es.uvigo.esei.daa; + +import static java.util.Collections.unmodifiableSet; + +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.ApplicationPath; + +import es.uvigo.esei.daa.filters.AuthorizationFilter; + +@ApplicationPath("/rest/*") +public class DAAExampleTestApplication extends DAAExampleApplication { + @Override + public Set> getClasses() { + final Set> classes = new HashSet<>(super.getClasses()); + + classes.add(AuthorizationFilter.class); + + return unmodifiableSet(classes); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/dataset/PeopleDataset.java b/src/test/java/es/uvigo/esei/daa/dataset/PeopleDataset.java new file mode 100644 index 0000000000000000000000000000000000000000..764a44f32ca073bfff43697da7e915ab6c0b4d9b --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/dataset/PeopleDataset.java @@ -0,0 +1,74 @@ +package es.uvigo.esei.daa.dataset; + +import static java.util.Arrays.binarySearch; +import static java.util.Arrays.stream; + +import java.util.Arrays; +import java.util.function.Predicate; + +import es.uvigo.esei.daa.entities.Person; + +public final class PeopleDataset { + private PeopleDataset() {} + + public static Person[] people() { + return new Person[] { + new Person(1, "Antón", "Álvarez"), + new Person(2, "Ana", "Amargo"), + new Person(3, "Manuel", "Martínez"), + new Person(4, "María", "Márquez"), + new Person(5, "Lorenzo", "López"), + new Person(6, "Laura", "Laredo"), + new Person(7, "Perico", "Palotes"), + new Person(8, "Patricia", "Pérez"), + new Person(9, "Julia", "Justa"), + new Person(10, "Juan", "Jiménez") + }; + } + + public static Person[] peopleWithout(int ... ids) { + Arrays.sort(ids); + + final Predicate hasValidId = person -> + binarySearch(ids, person.getId()) < 0; + + return stream(people()) + .filter(hasValidId) + .toArray(Person[]::new); + } + + public static Person person(int id) { + return stream(people()) + .filter(person -> person.getId() == id) + .findAny() + .orElseThrow(IllegalArgumentException::new); + } + + public static int existentId() { + return 5; + } + + public static int nonExistentId() { + return 1234; + } + + public static Person existentPerson() { + return person(existentId()); + } + + public static Person nonExistentPerson() { + return new Person(nonExistentId(), "Jane", "Smith"); + } + + public static String newName() { + return "John"; + } + + public static String newSurname() { + return "Doe"; + } + + public static Person newPerson() { + return new Person(people().length + 1, newName(), newSurname()); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/dataset/UsersDataset.java b/src/test/java/es/uvigo/esei/daa/dataset/UsersDataset.java new file mode 100644 index 0000000000000000000000000000000000000000..82cbcdeec1e23abcc76b0832958f33bce13c02df --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/dataset/UsersDataset.java @@ -0,0 +1,38 @@ +package es.uvigo.esei.daa.dataset; + +import java.util.Arrays; +import java.util.Base64; + +import es.uvigo.esei.daa.entities.User; + +public final class UsersDataset { + private UsersDataset() {} + + public static User[] users() { + return new User[] { + new User(adminLogin(), "713bfda78870bf9d1b261f565286f85e97ee614efe5f0faf7c34e7ca4f65baca", "ADMIN"), + new User(normalLogin(), "7bf24d6ca2242430343ab7e3efb89559a47784eea1123be989c1b2fb2ef66e83", "USER") + }; + } + + public static User user(String login) { + return Arrays.stream(users()) + .filter(user -> user.getLogin().equals(login)) + .findAny() + .orElseThrow(IllegalArgumentException::new); + } + + public static String adminLogin() { + return "admin"; + } + + public static String normalLogin() { + return "normal"; + } + + public static String userToken(String login) { + final String chain = login + ":" + login + "pass"; + + return Base64.getEncoder().encodeToString(chain.getBytes()); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/entities/PersonUnitTest.java b/src/test/java/es/uvigo/esei/daa/entities/PersonUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4b40545da0a5efabac07496a64071f0e62bf39ff --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/entities/PersonUnitTest.java @@ -0,0 +1,93 @@ +package es.uvigo.esei.daa.entities; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; + +public class PersonUnitTest { + @Test + public void testPersonIntStringString() { + final int id = 1; + final String name = "John"; + final String surname = "Doe"; + + final Person person = new Person(id, name, surname); + + assertThat(person.getId(), is(equalTo(id))); + assertThat(person.getName(), is(equalTo(name))); + assertThat(person.getSurname(), is(equalTo(surname))); + } + + @Test(expected = NullPointerException.class) + public void testPersonIntStringStringNullName() { + new Person(1, null, "Doe"); + } + + @Test(expected = NullPointerException.class) + public void testPersonIntStringStringNullSurname() { + new Person(1, "John", null); + } + + @Test + public void testSetName() { + final int id = 1; + final String surname = "Doe"; + + final Person person = new Person(id, "John", surname); + person.setName("Juan"); + + assertThat(person.getId(), is(equalTo(id))); + assertThat(person.getName(), is(equalTo("Juan"))); + assertThat(person.getSurname(), is(equalTo(surname))); + } + + @Test(expected = NullPointerException.class) + public void testSetNullName() { + final Person person = new Person(1, "John", "Doe"); + + person.setName(null); + } + + @Test + public void testSetSurname() { + final int id = 1; + final String name = "John"; + + final Person person = new Person(id, name, "Doe"); + person.setSurname("Dolores"); + + assertThat(person.getId(), is(equalTo(id))); + assertThat(person.getName(), is(equalTo(name))); + assertThat(person.getSurname(), is(equalTo("Dolores"))); + } + + @Test(expected = NullPointerException.class) + public void testSetNullSurname() { + final Person person = new Person(1, "John", "Doe"); + + person.setSurname(null); + } + + @Test + public void testEqualsObject() { + final Person personA = new Person(1, "Name A", "Surname A"); + final Person personB = new Person(1, "Name B", "Surname B"); + + assertTrue(personA.equals(personB)); + } + + @Test + public void testEqualsHashcode() { + EqualsVerifier.forClass(Person.class) + .withIgnoredFields("name", "surname") + .suppress(Warning.STRICT_INHERITANCE) + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/filters/AuthorizationFilter.java b/src/test/java/es/uvigo/esei/daa/filters/AuthorizationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..40400f7a873791dddbcb3f6e0b18dad524f1f7cf --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/filters/AuthorizationFilter.java @@ -0,0 +1,128 @@ +package es.uvigo.esei.daa.filters; + +import java.io.IOException; +import java.security.Principal; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.PathSegment; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.ext.Provider; + +import es.uvigo.esei.daa.dao.DAOException; +import es.uvigo.esei.daa.dao.UsersDAO; +import es.uvigo.esei.daa.entities.User; + +/** + * This performs the Basic HTTP authentication following (almost) the same + * rules as the defined in the web.xml file. + * + * @author Miguel Reboiro Jato + */ +@Provider +@Priority(Priorities.AUTHENTICATION) +public class AuthorizationFilter implements ContainerRequestFilter { + // Add here the list of REST paths that an administrator can access. + private final static List ADMIN_PATHS = Arrays.asList("people"); + + private final UsersDAO dao; + + public AuthorizationFilter() { + this.dao = new UsersDAO(); + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + // Get the authentication passed in HTTP headers parameters + final String auth = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); + + if (auth == null) { + requestContext.abortWith(createResponse()); + } else { + final byte[] decodedToken = Base64.getDecoder() + .decode(auth.substring(6)); + + final String userColonPass = new String(decodedToken); + final String[] userPass = userColonPass.split(":", 2); + + if (userPass.length == 2) { + try { + if (this.dao.checkLogin(userPass[0], userPass[1])) { + final User user = this.dao.get(userPass[0]); + + if (isAdminPath(requestContext) && !user.getRole().equals("ADMIN")) { + requestContext.abortWith(createResponse()); + } else { + requestContext.setSecurityContext(new UserSecurityContext(user)); + } + } else { + requestContext.abortWith(createResponse()); + } + } catch (DAOException e) { + requestContext.abortWith(createResponse()); + } + } else { + requestContext.abortWith(createResponse()); + } + } + } + + private static boolean isAdminPath(ContainerRequestContext context) { + final List pathSegments = context.getUriInfo().getPathSegments(); + + if (pathSegments.isEmpty()) { + return false; + } else { + final String path = pathSegments.get(0).getPath(); + return ADMIN_PATHS.contains(path); + } + } + + private static Response createResponse() { + return Response.status(Status.UNAUTHORIZED) + .header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"DAAExample\"") + .entity("Page requires login.") + .build(); + } + + private static final class UserSecurityContext implements SecurityContext { + private final User user; + + private UserSecurityContext(User user) { + this.user = user; + } + + @Override + public boolean isUserInRole(String role) { + return user.getRole().equals(role); + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public Principal getUserPrincipal() { + return new Principal() { + @Override + public String getName() { + return user.getLogin(); + } + }; + } + + @Override + public String getAuthenticationScheme() { + return SecurityContext.BASIC_AUTH; + } + } +} diff --git a/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBinding.java b/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBinding.java new file mode 100644 index 0000000000000000000000000000000000000000..e22da5ee48c5a1e173c0b13e839459a1c07bc326 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBinding.java @@ -0,0 +1,18 @@ +package es.uvigo.esei.daa.listeners; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(ApplicationContextBindings.class) +public @interface ApplicationContextBinding { + public String jndiUrl(); + public String name() default ""; + public Class type() default None.class; + + public final static class None {} +} diff --git a/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBindings.java b/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBindings.java new file mode 100644 index 0000000000000000000000000000000000000000..1b728ff2058a532b83e81dbbc58cce485d280787 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBindings.java @@ -0,0 +1,12 @@ +package es.uvigo.esei.daa.listeners; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApplicationContextBindings { + public ApplicationContextBinding[] value(); +} diff --git a/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextJndiBindingTestExecutionListener.java b/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextJndiBindingTestExecutionListener.java new file mode 100644 index 0000000000000000000000000000000000000000..0708cc00d9a041a4463988c5c9fe6f32676baa83 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextJndiBindingTestExecutionListener.java @@ -0,0 +1,44 @@ +package es.uvigo.esei.daa.listeners; + +import org.springframework.mock.jndi.SimpleNamingContextBuilder; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +import es.uvigo.esei.daa.listeners.ApplicationContextBinding.None; + + +public class ApplicationContextJndiBindingTestExecutionListener extends AbstractTestExecutionListener { + private SimpleNamingContextBuilder contextBuilder; + + @Override + public void beforeTestClass(TestContext testContext) throws Exception { + final Class testClass = testContext.getTestClass(); + + final ApplicationContextBinding[] bindings = testClass.getAnnotationsByType(ApplicationContextBinding.class); + + this.contextBuilder = SimpleNamingContextBuilder.emptyActivatedContextBuilder(); + for (ApplicationContextBinding binding : bindings) { + final String bindingName = binding.name(); + final Class bindingType = binding.type(); + + Object bean; + if (bindingName.isEmpty() && bindingType.equals(None.class)) { + throw new IllegalArgumentException("name or type attributes must be configured in ApplicationContextBinding"); + } else if (bindingName.isEmpty()) { + bean = testContext.getApplicationContext().getBean(bindingType); + } else if (bindingType.equals(None.class)) { + bean = testContext.getApplicationContext().getBean(bindingName); + } else { + bean = testContext.getApplicationContext().getBean(bindingName, bindingType); + } + + this.contextBuilder.bind(binding.jndiUrl(), bean); + } + } + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + this.contextBuilder.clear(); + this.contextBuilder = null; + } +} diff --git a/src/test/java/es/uvigo/esei/daa/listeners/DbManagement.java b/src/test/java/es/uvigo/esei/daa/listeners/DbManagement.java new file mode 100644 index 0000000000000000000000000000000000000000..13bb496c9d48692f3a2fe6fc9af9a3bb6ad91ddc --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/listeners/DbManagement.java @@ -0,0 +1,14 @@ +package es.uvigo.esei.daa.listeners; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DbManagement { + public String[] create() default ""; + public String[] drop() default ""; + public DbManagementAction action() default DbManagementAction.CREATE_DROP; +} diff --git a/src/test/java/es/uvigo/esei/daa/listeners/DbManagementAction.java b/src/test/java/es/uvigo/esei/daa/listeners/DbManagementAction.java new file mode 100644 index 0000000000000000000000000000000000000000..0e23a1c5e9dd556be92f66245647b774c8af3b49 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/listeners/DbManagementAction.java @@ -0,0 +1,5 @@ +package es.uvigo.esei.daa.listeners; + +public enum DbManagementAction { + DROP_CREATE_DROP, CREATE_DROP, ONLY_CREATE, ONLY_DROP; +} diff --git a/src/test/java/es/uvigo/esei/daa/listeners/DbManagementTestExecutionListener.java b/src/test/java/es/uvigo/esei/daa/listeners/DbManagementTestExecutionListener.java new file mode 100644 index 0000000000000000000000000000000000000000..6ec05f6b5833431d0b22c052f466f6e72dc85075 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/listeners/DbManagementTestExecutionListener.java @@ -0,0 +1,113 @@ +package es.uvigo.esei.daa.listeners; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +public class DbManagementTestExecutionListener extends AbstractTestExecutionListener { + private DbManagement configuration; + private DataSource datasource; + + @Override + public void beforeTestClass(TestContext testContext) throws Exception { + final Class testClass = testContext.getTestClass(); + this.configuration = testClass.getAnnotation(DbManagement.class); + + if (this.configuration == null) + throw new IllegalStateException(String.format( + "Missing %s annotation in %s class", + DbManagement.class.getSimpleName(), testClass.getName() + )); + + this.datasource = testContext.getApplicationContext().getBean(DataSource.class); + + switch (this.configuration.action()) { + case DROP_CREATE_DROP: + executeDrop(); + case CREATE_DROP: + case ONLY_CREATE: + executeCreate(); + break; + default: + } + } + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + try { + switch (this.configuration.action()) { + case DROP_CREATE_DROP: + case CREATE_DROP: + case ONLY_DROP: + executeDrop(); + break; + default: + } + } finally { + this.configuration = null; + this.datasource = null; + } + } + + private void executeCreate() throws SQLException, IOException { + this.executeQueries(configuration.create()); + } + + private void executeDrop() throws SQLException, IOException { + this.executeQueries(configuration.drop()); + } + + private void executeQueries(String ... queriesPaths) + throws SQLException, IOException { + try (Connection connection = this.datasource.getConnection()) { + try (Statement statement = connection.createStatement()) { + for (String queryPath : queriesPaths) { + final String queries = readFile(queryPath); + for (String query : queries.split(";")) { + query = query.trim(); + if (!query.trim().isEmpty()) { + statement.addBatch(query); + } + } + } + statement.executeBatch(); + } + } + } + + private static String readFile(String path) throws IOException { + final String classpathPrefix = "classpath:"; + + if (path.startsWith(classpathPrefix)) { + path = path.substring(classpathPrefix.length()); + + final ClassLoader classLoader = + DbManagementTestExecutionListener.class.getClassLoader(); + + final InputStream fileIS = classLoader.getResourceAsStream(path); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileIS))) { + final StringBuilder sb = new StringBuilder(); + + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + + return sb.toString(); + } + } else { + return new String(Files.readAllBytes(Paths.get(path))); + } + } +} diff --git a/src/test/java/es/uvigo/esei/daa/matchers/HasHttpStatus.java b/src/test/java/es/uvigo/esei/daa/matchers/HasHttpStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..8da8cb196f99243c5dbf04e183fb232ad475f2be --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/matchers/HasHttpStatus.java @@ -0,0 +1,95 @@ +/* + * #%L + * PanDrugsDB Backend + * %% + * Copyright (C) 2015 Fátima Al-Shahrour, Elena Piñeiro, Daniel Glez-Peña and Miguel Reboiro-Jato + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package es.uvigo.esei.daa.matchers; + +import static java.util.Objects.requireNonNull; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.Response.StatusType; + +import org.hamcrest.Description; +import org.hamcrest.Factory; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +public class HasHttpStatus extends TypeSafeMatcher { + private final StatusType expectedStatus; + + public HasHttpStatus(StatusType expectedStatus) { + this.expectedStatus = requireNonNull(expectedStatus); + } + + public HasHttpStatus(int expectedStatus) { + this(Status.fromStatusCode(expectedStatus)); + } + + @Override + public void describeTo(Description description) { + description.appendValue(this.expectedStatus); + } + + @Override + protected void describeMismatchSafely(Response item, Description mismatchDescription) { + mismatchDescription.appendText("was ").appendValue(item.getStatusInfo()); + } + + @Override + protected boolean matchesSafely(Response item) { + return item != null && expectedStatus.getStatusCode() == item.getStatusInfo().getStatusCode(); + } + + @Factory + public static Matcher hasHttpStatus(StatusType expectedStatus) { + return new HasHttpStatus(expectedStatus); + } + + @Factory + public static Matcher hasHttpStatus(int expectedStatus) { + return new HasHttpStatus(expectedStatus); + } + + @Factory + public static Matcher hasOkStatus() { + return new HasHttpStatus(Response.Status.OK); + } + + @Factory + public static Matcher hasBadRequestStatus() { + return new HasHttpStatus(Response.Status.BAD_REQUEST); + } + + @Factory + public static Matcher hasInternalServerErrorStatus() { + return new HasHttpStatus(Response.Status.INTERNAL_SERVER_ERROR); + } + + @Factory + public static Matcher hasUnauthorized() { + return new HasHttpStatus(Response.Status.UNAUTHORIZED); + } + + @Factory + public static Matcher hasForbidden() { + return new HasHttpStatus(Response.Status.FORBIDDEN); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToEntity.java b/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..a85dbc5c84f3c2eb2d182b7dfffc73a851be2123 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToEntity.java @@ -0,0 +1,364 @@ +package es.uvigo.esei.daa.matchers; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * An abstract {@link Matcher} that can be used to create new matchers that + * compare entities by their attributes. + * + * @author Miguel Reboiro Jato + * + * @param the type of the entities to be matched. + */ +public abstract class IsEqualToEntity extends TypeSafeMatcher { + /** + * The expected entity. + */ + protected final T expected; + + private Consumer describeTo; + + /** + * Constructs a new instance of {@link IsEqualToEntity}. + * + * @param entity the expected tentity. + */ + public IsEqualToEntity(final T entity) { + this.expected = requireNonNull(entity); + } + + @Override + public void describeTo(final Description description) { + if (this.describeTo != null) + this.describeTo.accept(description); + } + + /** + * Adds a new description using the template: + *

+ * {@code entity with value '' for } + *

+ * + * @param attribute the name of the attribute compared. + * @param expected the expected value. + */ + protected void addTemplatedDescription(final String attribute, final Object expected) { + this.describeTo = d -> d.appendText(String.format( + "%s entity with value '%s' for %s", + this.expected.getClass().getSimpleName(), + expected, attribute + )); + } + + /** + * Adds as the description of this matcher the + * {@link Matcher#describeTo(Description)} method of other matcher. + * + * @param matcher the matcher whose description will be used. + */ + protected void addMatcherDescription(final Matcher matcher) { + this.describeTo = matcher::describeTo; + } + + /** + * Cleans the current description. + */ + protected void clearDescribeTo() { + this.describeTo = null; + } + + protected boolean checkAttribute( + final String attribute, + final Function getter, final T actual, + final Function> matcherFactory + ) { + final R expectedValue = getter.apply(this.expected); + final R actualValue = getter.apply(actual); + + if (expectedValue == null && actualValue == null) { + return true; + } else if (expectedValue == null || actualValue == null) { + this.addTemplatedDescription(attribute, expectedValue); + return false; + } else { + final Matcher matcher = matcherFactory.apply(expectedValue); + if (matcher.matches(actualValue)) { + return true; + } else { + this.addMatcherDescription(matcher); + + return false; + } + } + } + + /** + * Compares the expected and the actual value of an array attribute. The + * elements of the attribute will be checked using a custom matcher. + * If the comparison fails, the description of the error will be updated. + * + * @param attribute the name of the attribute compared. + * @param getter the getter function of the attribute. + * @param actual the actual entity being compared to the expected entity. + * @param matcherFactory a function that creates a matcher for the expected + * array values. + * @param type of the value returned by the getter. + * @return {@code true} if the value of the expected and actual attributes + * are equals and {@code false} otherwise. If the result is {@code false}, + * the current description will be updated. + */ + protected boolean checkArrayAttribute( + final String attribute, + final Function getter, final T actual, + final Function>> matcherFactory + ) { + final R[] expectedValue = getter.apply(this.expected); + final R[] actualValue = getter.apply(actual); + + if (expectedValue == null && actualValue == null) { + return true; + } else if (expectedValue == null || actualValue == null) { + this.addTemplatedDescription(attribute, expectedValue); + return false; + } else { + final Matcher> matcher = + matcherFactory.apply(expectedValue); + + if (matcher.matches(asList(actualValue))) { + return true; + } else { + this.addMatcherDescription(matcher); + + return false; + } + } + } + + /** + * Compares the expected and the actual value of an iterable attribute. The + * elements of the attribute will be checked using a custom matcher. + * If the comparison fails, the description of the error will be updated. + * + * @param attribute the name of the attribute compared. + * @param getter the getter function of the attribute. + * @param actual the actual entity being compared to the expected entity. + * @param matcherFactory a function that creates a matcher for the expected + * iterable values. + * @param type of the value returned by the getter. + * @return {@code true} if the value of the expected and actual attributes + * are equals and {@code false} otherwise. If the result is {@code false}, + * the current description will be updated. + */ + protected boolean checkIterableAttribute( + final String attribute, + final Function> getter, final T actual, + final Function, Matcher>> matcherFactory + ) { + final Iterable expectedValue = getter.apply(this.expected); + final Iterable actualValue = getter.apply(actual); + + if (expectedValue == null && actualValue == null) { + return true; + } else if (expectedValue == null || actualValue == null) { + this.addTemplatedDescription(attribute, expectedValue); + return false; + } else { + final Matcher> matcher = + matcherFactory.apply(expectedValue); + + if (matcher.matches(actualValue)) { + return true; + } else { + this.addMatcherDescription(matcher); + + return false; + } + } + } + + /** + * Compares the expected and the actual value of an attribute. If the + * comparison fails, the description of the error will be updated. + * + * @param attribute the name of the attribute compared. + * @param getter the getter function of the attribute. + * @param actual the actual entity being compared to the expected entity. + * @param type of the value returned by the getter. + * @return {@code true} if the value of the expected and actual attributes + * are equals and {@code false} otherwise. If the result is {@code false}, + * the current description will be updated. + */ + protected boolean checkAttribute( + final String attribute, final Function getter, final T actual + ) { + final R expectedValue = getter.apply(this.expected); + final R actualValue = getter.apply(actual); + + if (expectedValue == null && actualValue == null) { + return true; + } else if (expectedValue == null || !expectedValue.equals(actualValue)) { + this.addTemplatedDescription(attribute, expectedValue); + return false; + } else { + return true; + } + } + + /** + * Compares the expected and the actual value of an array attribute. If the + * comparison fails, the description of the error will be updated. + * + * @param attribute the name of the attribute compared. + * @param getter the getter function of the attribute. + * @param actual the actual entity being compared to the expected entity. + * @param type of the value returned by the getter. + * @return {@code true} if the value of the expected and actual attributes + * are equals and {@code false} otherwise. If the result is {@code false}, + * the current description will be updated. + */ + protected boolean checkArrayAttribute( + final String attribute, final Function getter, final T actual + ) { + final R[] expectedValue = getter.apply(this.expected); + final R[] actualValue = getter.apply(actual); + + if (expectedValue == null && actualValue == null) { + return true; + } else if (expectedValue == null || actualValue == null) { + this.addTemplatedDescription(attribute, expectedValue == null ? "null" : Arrays.toString(expectedValue)); + return false; + } else if (!Arrays.equals(expectedValue, actualValue)) { + this.addTemplatedDescription(attribute, Arrays.toString(expectedValue)); + return false; + } else + return true; + } + + /** + * Compares the expected and the actual value of an int array attribute. If + * the comparison fails, the description of the error will be updated. + * + * @param attribute the name of the attribute compared. + * @param getter the getter function of the attribute. + * @param actual the actual entity being compared to the expected entity. + * @param type of the value returned by the getter. + * @return {@code true} if the value of the expected and actual attributes + * are equals and {@code false} otherwise. If the result is {@code false}, + * the current description will be updated. + */ + protected boolean checkIntArrayAttribute( + final String attribute, final Function getter, final T actual + ) { + final int[] expectedValue = getter.apply(this.expected); + final int[] actualValue = getter.apply(actual); + + if (expectedValue == null && actualValue == null) { + return true; + } else if (expectedValue == null || actualValue == null) { + this.addTemplatedDescription(attribute, expectedValue == null ? "null" : Arrays.toString(expectedValue)); + return false; + } else if (!Arrays.equals(expectedValue, actualValue)) { + this.addTemplatedDescription(attribute, Arrays.toString(expectedValue)); + return false; + } else + return true; + } + + /** + * Utility method that generates a {@link Matcher} that compares several + * entities. + * + * @param converter a function to create a matcher for an entity. + * @param entities the entities to be used as the expected values. + * @param type of the entity. + * @return a new {@link Matcher} that compares several entities. + */ + @SafeVarargs + protected static Matcher> containsEntityInAnyOrder( + final Function> converter, final T ... entities + ) { + final Collection> entitiesMatchers = stream(entities) + .map(converter) + .collect(toList()); + + return containsInAnyOrder(entitiesMatchers); + } + + /** + * Utility method that generates a {@link Matcher} that compares several + * entities. + * + * @param converter a function to create a matcher for an entity. + * @param entities the entities to be used as the expected values. + * @param type of the entity. + * @return a new {@link Matcher} that compares several entities. + */ + protected static Matcher> containsEntityInAnyOrder( + final Function> converter, final Iterable entities + ) { + final Collection> entitiesMatchers = + StreamSupport.stream(entities.spliterator(), false) + .map(converter) + .collect(toList()); + + return containsInAnyOrder(entitiesMatchers); + } + + /** + * Utility method that generates a {@link Matcher} that compares several + * entities in the same received order. + * + * @param converter A function to create a matcher for an entity. + * @param entities The entities to be used as the expected values, in the + * order to be compared. + * @param The type of the entity. + * + * @return A new {@link Matcher} that compares several entities in the same + * received order. + */ + @SafeVarargs + protected static Matcher> containsEntityInOrder( + final Function> converter, final T ... entities + ) { + return contains(stream(entities).map(converter).collect(toList())); + } + + /** + * Utility method that generates a {@link Matcher} that compares several + * entities in the same received order. + * + * @param converter A function to create a matcher for an entity. + * @param entities The entities to be used as the expected values, in the + * order to be compared. + * @param The type of the entity. + * + * @return A new {@link Matcher} that compares several entities in the same + * received order. + */ + protected static Matcher> containsEntityInOrder( + final Function> converter, final Iterable entities + ) { + final List> matchersList = + StreamSupport.stream(entities.spliterator(), false) + .map(converter) + .collect(toList()); + + return contains(matchersList); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToPerson.java b/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToPerson.java new file mode 100644 index 0000000000000000000000000000000000000000..6c06e5f0ff2b8d7e69d96da15b7af78cfd3c62e3 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToPerson.java @@ -0,0 +1,56 @@ +package es.uvigo.esei.daa.matchers; + +import org.hamcrest.Factory; +import org.hamcrest.Matcher; + +import es.uvigo.esei.daa.entities.Person; + +public class IsEqualToPerson extends IsEqualToEntity { + public IsEqualToPerson(Person entity) { + super(entity); + } + + @Override + protected boolean matchesSafely(Person actual) { + this.clearDescribeTo(); + + if (actual == null) { + this.addTemplatedDescription("actual", expected.toString()); + return false; + } else { + return checkAttribute("id", Person::getId, actual) + && checkAttribute("name", Person::getName, actual) + && checkAttribute("surname", Person::getSurname, actual); + } + } + + /** + * Factory method that creates a new {@link IsEqualToEntity} matcher with + * the provided {@link Person} as the expected value. + * + * @param person the expected person. + * @return a new {@link IsEqualToEntity} matcher with the provided + * {@link Person} as the expected value. + */ + @Factory + public static IsEqualToPerson equalsToPerson(Person person) { + return new IsEqualToPerson(person); + } + + /** + * Factory method that returns a new {@link Matcher} that includes several + * {@link IsEqualToPerson} matchers, each one using an {@link Person} of the + * provided ones as the expected value. + * + * @param persons the persons to be used as the expected values. + * @return a new {@link Matcher} that includes several + * {@link IsEqualToPerson} matchers, each one using an {@link Person} of the + * provided ones as the expected value. + * @see IsEqualToEntity#containsEntityInAnyOrder(java.util.function.Function, Object...) + */ + @Factory + public static Matcher> containsPeopleInAnyOrder(Person ... persons) { + return containsEntityInAnyOrder(IsEqualToPerson::equalsToPerson, persons); + } + +} diff --git a/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToUser.java b/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToUser.java new file mode 100644 index 0000000000000000000000000000000000000000..689674ea1e7e97580ce16377bc3109a668e4768c --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/matchers/IsEqualToUser.java @@ -0,0 +1,56 @@ +package es.uvigo.esei.daa.matchers; + +import org.hamcrest.Factory; +import org.hamcrest.Matcher; + +import es.uvigo.esei.daa.entities.Person; +import es.uvigo.esei.daa.entities.User; + +public class IsEqualToUser extends IsEqualToEntity { + public IsEqualToUser(User entity) { + super(entity); + } + + @Override + protected boolean matchesSafely(User actual) { + this.clearDescribeTo(); + + if (actual == null) { + this.addTemplatedDescription("actual", expected.toString()); + return false; + } else { + return checkAttribute("login", User::getLogin, actual) + && checkAttribute("password", User::getPassword, actual); + } + } + + /** + * Factory method that creates a new {@link IsEqualToEntity} matcher with + * the provided {@link Person} as the expected value. + * + * @param user the expected person. + * @return a new {@link IsEqualToEntity} matcher with the provided + * {@link Person} as the expected value. + */ + @Factory + public static IsEqualToUser equalsToUser(User user) { + return new IsEqualToUser(user); + } + + /** + * Factory method that returns a new {@link Matcher} that includes several + * {@link IsEqualToUser} matchers, each one using an {@link Person} of the + * provided ones as the expected value. + * + * @param users the persons to be used as the expected values. + * @return a new {@link Matcher} that includes several + * {@link IsEqualToUser} matchers, each one using an {@link Person} of the + * provided ones as the expected value. + * @see IsEqualToEntity#containsEntityInAnyOrder(java.util.function.Function, Object...) + */ + @Factory + public static Matcher> containsPeopleInAnyOrder(User ... users) { + return containsEntityInAnyOrder(IsEqualToUser::equalsToUser, users); + } + +} diff --git a/src/test/java/es/uvigo/esei/daa/rest/PeopleResourceTest.java b/src/test/java/es/uvigo/esei/daa/rest/PeopleResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1e0263898211837ad5cc6058fc9b4d6856c1a8a5 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/rest/PeopleResourceTest.java @@ -0,0 +1,291 @@ +package es.uvigo.esei.daa.rest; + +import static es.uvigo.esei.daa.dataset.PeopleDataset.existentId; +import static es.uvigo.esei.daa.dataset.PeopleDataset.existentPerson; +import static es.uvigo.esei.daa.dataset.PeopleDataset.newName; +import static es.uvigo.esei.daa.dataset.PeopleDataset.newPerson; +import static es.uvigo.esei.daa.dataset.PeopleDataset.newSurname; +import static es.uvigo.esei.daa.dataset.PeopleDataset.nonExistentId; +import static es.uvigo.esei.daa.dataset.PeopleDataset.people; +import static es.uvigo.esei.daa.dataset.UsersDataset.adminLogin; +import static es.uvigo.esei.daa.dataset.UsersDataset.normalLogin; +import static es.uvigo.esei.daa.dataset.UsersDataset.userToken; +import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasBadRequestStatus; +import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasOkStatus; +import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasUnauthorized; +import static es.uvigo.esei.daa.matchers.IsEqualToPerson.containsPeopleInAnyOrder; +import static es.uvigo.esei.daa.matchers.IsEqualToPerson.equalsToPerson; +import static javax.ws.rs.client.Entity.entity; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.IOException; +import java.util.List; + +import javax.sql.DataSource; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.ExpectedDatabase; + +import es.uvigo.esei.daa.DAAExampleTestApplication; +import es.uvigo.esei.daa.entities.Person; +import es.uvigo.esei.daa.listeners.ApplicationContextBinding; +import es.uvigo.esei.daa.listeners.ApplicationContextJndiBindingTestExecutionListener; +import es.uvigo.esei.daa.listeners.DbManagement; +import es.uvigo.esei.daa.listeners.DbManagementTestExecutionListener; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:contexts/mem-context.xml") +@TestExecutionListeners({ + DbUnitTestExecutionListener.class, + DbManagementTestExecutionListener.class, + ApplicationContextJndiBindingTestExecutionListener.class +}) +@ApplicationContextBinding( + jndiUrl = "java:/comp/env/jdbc/daaexample", + type = DataSource.class +) +@DbManagement( + create = "classpath:db/hsqldb.sql", + drop = "classpath:db/hsqldb-drop.sql" +) +@DatabaseSetup("/datasets/dataset.xml") +@ExpectedDatabase("/datasets/dataset.xml") +public class PeopleResourceTest extends JerseyTest { + @Override + protected Application configure() { + return new DAAExampleTestApplication(); + } + + @Override + protected void configureClient(ClientConfig config) { + super.configureClient(config); + + // Enables JSON transformation in client + config.register(JacksonJsonProvider.class); + config.property("com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE); + } + + @Test + public void testList() throws IOException { + final Response response = target("people").request() + .header("Authorization", "Basic " + userToken(adminLogin())) + .get(); + assertThat(response, hasOkStatus()); + + final List people = response.readEntity(new GenericType>(){}); + + assertThat(people, containsPeopleInAnyOrder(people())); + } + + @Test + public void testListUnauthorized() throws IOException { + final Response response = target("people").request() + .header("Authorization", "Basic " + userToken(normalLogin())) + .get(); + assertThat(response, hasUnauthorized()); + } + + @Test + public void testGet() throws IOException { + final Response response = target("people/" + existentId()).request() + .header("Authorization", "Basic " + userToken(adminLogin())) + .get(); + assertThat(response, hasOkStatus()); + + final Person person = response.readEntity(Person.class); + + assertThat(person, is(equalsToPerson(existentPerson()))); + } + + @Test + public void testGetUnauthorized() throws IOException { + final Response response = target("people/" + existentId()).request() + .header("Authorization", "Basic " + userToken(normalLogin())) + .get(); + assertThat(response, hasUnauthorized()); + } + + @Test + public void testGetInvalidId() throws IOException { + final Response response = target("people/" + nonExistentId()).request() + .header("Authorization", "Basic " + userToken(adminLogin())) + .get(); + + assertThat(response, hasBadRequestStatus()); + } + + @Test + @ExpectedDatabase("/datasets/dataset-add.xml") + public void testAdd() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + form.param("surname", newSurname()); + + final Response response = target("people").request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + assertThat(response, hasOkStatus()); + + final Person person = response.readEntity(Person.class); + + assertThat(person, is(equalsToPerson(newPerson()))); + } + + @Test + public void testAddUnauthorized() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + form.param("surname", newSurname()); + + final Response response = target("people").request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(normalLogin())) + .post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasUnauthorized()); + } + + @Test + public void testAddMissingName() throws IOException { + final Form form = new Form(); + form.param("surname", newSurname()); + + final Response response = target("people").request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasBadRequestStatus()); + } + + @Test + public void testAddMissingSurname() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + + final Response response = target("people").request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .post(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasBadRequestStatus()); + } + + @Test + @ExpectedDatabase("/datasets/dataset-modify.xml") + public void testModify() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + form.param("surname", newSurname()); + + final Response response = target("people/" + existentId()).request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + assertThat(response, hasOkStatus()); + + final Person modifiedPerson = response.readEntity(Person.class); + + final Person person = existentPerson(); + person.setName(newName()); + person.setSurname(newSurname()); + + assertThat(modifiedPerson, is(equalsToPerson(person))); + } + + @Test + public void testModifyUnauthorized() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + form.param("surname", newSurname()); + + final Response response = target("people/" + existentId()).request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(normalLogin())) + .put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasUnauthorized()); + } + + @Test + public void testModifyName() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + + final Response response = target("people/" + existentId()).request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasBadRequestStatus()); + } + + @Test + public void testModifySurname() throws IOException { + final Form form = new Form(); + form.param("surname", newSurname()); + + final Response response = target("people/" + existentId()).request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .put(entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasBadRequestStatus()); + } + + @Test + public void testModifyInvalidId() throws IOException { + final Form form = new Form(); + form.param("name", newName()); + form.param("surname", newSurname()); + + final Response response = target("people/" + nonExistentId()).request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Basic " + userToken(adminLogin())) + .put(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE)); + + assertThat(response, hasBadRequestStatus()); + } + + @Test + @ExpectedDatabase("/datasets/dataset-delete.xml") + public void testDelete() throws IOException { + final Response response = target("people/" + existentId()).request() + .header("Authorization", "Basic " + userToken(adminLogin())) + .delete(); + + assertThat(response, hasOkStatus()); + + final Integer deletedId = response.readEntity(Integer.class); + + assertThat(deletedId, is(equalTo(existentId()))); + } + + @Test + public void testDeleteUnauthorized() throws IOException { + final Response response = target("people/" + existentId()).request() + .header("Authorization", "Basic " + userToken(normalLogin())) + .delete(); + + assertThat(response, hasUnauthorized()); + } + + @Test + public void testDeleteInvalidId() throws IOException { + final Response response = target("people/" + nonExistentId()).request() + .header("Authorization", "Basic " + userToken(adminLogin())) + .delete(); + + assertThat(response, hasBadRequestStatus()); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/rest/UsersResourceTest.java b/src/test/java/es/uvigo/esei/daa/rest/UsersResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..253ab07eed32f6fc94b5029b72472f80dc15db53 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/rest/UsersResourceTest.java @@ -0,0 +1,138 @@ +package es.uvigo.esei.daa.rest; + +import static es.uvigo.esei.daa.dataset.UsersDataset.adminLogin; +import static es.uvigo.esei.daa.dataset.UsersDataset.normalLogin; +import static es.uvigo.esei.daa.dataset.UsersDataset.user; +import static es.uvigo.esei.daa.dataset.UsersDataset.userToken; +import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasOkStatus; +import static es.uvigo.esei.daa.matchers.HasHttpStatus.hasUnauthorized; +import static es.uvigo.esei.daa.matchers.IsEqualToUser.equalsToUser; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.IOException; + +import javax.sql.DataSource; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; +import com.github.springtestdbunit.annotation.ExpectedDatabase; + +import es.uvigo.esei.daa.DAAExampleTestApplication; +import es.uvigo.esei.daa.entities.User; +import es.uvigo.esei.daa.listeners.ApplicationContextBinding; +import es.uvigo.esei.daa.listeners.ApplicationContextJndiBindingTestExecutionListener; +import es.uvigo.esei.daa.listeners.DbManagement; +import es.uvigo.esei.daa.listeners.DbManagementTestExecutionListener; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:contexts/mem-context.xml") +@TestExecutionListeners({ + DbUnitTestExecutionListener.class, + DbManagementTestExecutionListener.class, + ApplicationContextJndiBindingTestExecutionListener.class +}) +@ApplicationContextBinding( + jndiUrl = "java:/comp/env/jdbc/daaexample", + type = DataSource.class +) +@DbManagement( + create = "classpath:db/hsqldb.sql", + drop = "classpath:db/hsqldb-drop.sql" +) +@DatabaseSetup("/datasets/dataset.xml") +@ExpectedDatabase("/datasets/dataset.xml") +public class UsersResourceTest extends JerseyTest { + @Override + protected Application configure() { + return new DAAExampleTestApplication(); + } + + @Override + protected void configureClient(ClientConfig config) { + super.configureClient(config); + + // Enables JSON transformation in client + config.register(JacksonJsonProvider.class); + config.property("com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE); + } + + @Test + public void testGetAdminOwnUser() throws IOException { + final String admin = adminLogin(); + + final Response response = target("users/" + admin).request() + .header("Authorization", "Basic " + userToken(admin)) + .get(); + assertThat(response, hasOkStatus()); + + final User user = response.readEntity(User.class); + + assertThat(user, is(equalsToUser(user(admin)))); + } + + @Test + public void testGetAdminOtherUser() throws IOException { + final String admin = adminLogin(); + final String otherUser = normalLogin(); + + final Response response = target("users/" + otherUser).request() + .header("Authorization", "Basic " + userToken(admin)) + .get(); + assertThat(response, hasOkStatus()); + + final User user = response.readEntity(User.class); + + assertThat(user, is(equalsToUser(user(otherUser)))); + } + + @Test + public void testGetNormalOwnUser() throws IOException { + final String login = normalLogin(); + + final Response response = target("users/" + login).request() + .header("Authorization", "Basic " + userToken(login)) + .get(); + assertThat(response, hasOkStatus()); + + final User user = response.readEntity(User.class); + + assertThat(user, is(equalsToUser(user(login)))); + } + + @Test + public void testGetNoCredentials() throws IOException { + final Response response = target("users/" + normalLogin()).request().get(); + + assertThat(response, hasUnauthorized()); + } + + @Test + public void testGetBadCredentials() throws IOException { + final Response response = target("users/" + adminLogin()).request() + .header("Authorization", "Basic YmFkOmNyZWRlbnRpYWxz") + .get(); + + assertThat(response, hasUnauthorized()); + } + + @Test + public void testGetIllegalAccess() throws IOException { + final Response response = target("users/" + adminLogin()).request() + .header("Authorization", "Basic " + userToken(normalLogin())) + .get(); + + assertThat(response, hasUnauthorized()); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/suites/IntegrationTestSuite.java b/src/test/java/es/uvigo/esei/daa/suites/IntegrationTestSuite.java new file mode 100644 index 0000000000000000000000000000000000000000..3f0c667c53d5328167d72f6d3b183451637862e3 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/suites/IntegrationTestSuite.java @@ -0,0 +1,16 @@ +package es.uvigo.esei.daa.suites; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import es.uvigo.esei.daa.rest.PeopleResourceTest; +import es.uvigo.esei.daa.rest.UsersResourceTest; + +@SuiteClasses({ + PeopleResourceTest.class, + UsersResourceTest.class +}) +@RunWith(Suite.class) +public class IntegrationTestSuite { +} diff --git a/src/test/java/es/uvigo/esei/daa/suites/UnitTestSuite.java b/src/test/java/es/uvigo/esei/daa/suites/UnitTestSuite.java new file mode 100644 index 0000000000000000000000000000000000000000..a08e9656929697d95a0416f42d04f70d257d6843 --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/suites/UnitTestSuite.java @@ -0,0 +1,14 @@ +package es.uvigo.esei.daa.suites; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import es.uvigo.esei.daa.entities.PersonUnitTest; + +@SuiteClasses({ + PersonUnitTest.class +}) +@RunWith(Suite.class) +public class UnitTestSuite { +} diff --git a/src/test/java/es/uvigo/esei/daa/util/ContextUtils.java b/src/test/java/es/uvigo/esei/daa/util/ContextUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..331044f0f1f201bf6d93b2805d08ac4c85031b4c --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/util/ContextUtils.java @@ -0,0 +1,24 @@ +package es.uvigo.esei.daa.util; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.springframework.mock.jndi.SimpleNamingContextBuilder; + +public final class ContextUtils { + private final static SimpleNamingContextBuilder CONTEXT_BUILDER = + new SimpleNamingContextBuilder(); + + private ContextUtils() {} + + public static void createFakeContext(DataSource datasource) + throws IllegalStateException, NamingException { + CONTEXT_BUILDER.bind("java:/comp/env/jdbc/daaexample", datasource); + CONTEXT_BUILDER.activate(); + } + + public static void clearContextBuilder() { + CONTEXT_BUILDER.clear(); + CONTEXT_BUILDER.deactivate(); + } +} diff --git a/src/test/java/es/uvigo/esei/daa/util/DatabaseQueryUnitTest.java b/src/test/java/es/uvigo/esei/daa/util/DatabaseQueryUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e541cb7ce2edb3b91ed6c40164b296ef10a3f34c --- /dev/null +++ b/src/test/java/es/uvigo/esei/daa/util/DatabaseQueryUnitTest.java @@ -0,0 +1,106 @@ +package es.uvigo.esei.daa.util; + +import static org.easymock.EasyMock.anyString; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; + +import java.sql.Connection; +import java.sql.ResultSet; + +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Before; + +import com.mysql.jdbc.PreparedStatement; + +/** + * Super-class for unit tests in the DAO layer. + * + *

The default {@link DatabaseQueryUnitTest#setUp()} method in this class + * create mocks for the datasource, connection, statement, and result variables + * that can be used by the DAO object under test.

+ * + * @author Miguel Reboiro Jato + */ +public abstract class DatabaseQueryUnitTest { + protected DataSource datasource; + protected Connection connection; + protected PreparedStatement statement; + protected ResultSet result; + + protected boolean verify; + + /** + * Configures the mocks and enables the verification. + * + * @throws Exception if an error happens while configuring the mocks. + */ + @Before + public void setUp() throws Exception { + datasource = createMock(DataSource.class); + connection = createMock(Connection.class); + statement = createNiceMock(PreparedStatement.class); + result = createMock(ResultSet.class); + + expect(datasource.getConnection()) + .andReturn(connection); + expect(connection.prepareStatement(anyString())) + .andReturn(statement) + .anyTimes(); // statement is optional + expect(statement.executeQuery()) + .andReturn(result) + .anyTimes(); // executeQuery is optional + statement.close(); + connection.close(); + + verify = true; + } + + /** + * Removes the default behavior of the mock instances and disables the mock + * verification. + */ + protected void resetAll() { + reset(result, statement, connection, datasource); + verify = false; + } + + /** + * Replays the configured behavior of the mock instances and enables the + * mock verification. The mocked datasource is also added to a new context. + */ + protected void replayAll() + throws Exception { + replay(result, statement, connection, datasource); + verify = true; + + ContextUtils.createFakeContext(datasource); + } + + /** + * Clears the context and verifies the mocks if the verification is enabled. + * + * @throws Exception if an error happens during verification. + */ + @After + public void tearDown() throws Exception { + ContextUtils.clearContextBuilder(); + + try { + if (verify) { + verify(datasource, connection, statement, result); + verify = false; + } + } finally { + datasource = null; + connection = null; + statement = null; + result = null; + } + } +} diff --git a/src/test/resources/contexts/hsql-context.xml b/src/test/resources/contexts/hsql-context.xml new file mode 100644 index 0000000000000000000000000000000000000000..0e1ec2eef039755bb68f9542a745806772e5cf22 --- /dev/null +++ b/src/test/resources/contexts/hsql-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/contexts/mem-context.xml b/src/test/resources/contexts/mem-context.xml new file mode 100644 index 0000000000000000000000000000000000000000..a37be3358a4bdc5d6e9e630b12210a2da842f9d2 --- /dev/null +++ b/src/test/resources/contexts/mem-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/datasets/dataset-add.xml b/src/test/resources/datasets/dataset-add.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a75a999a98510f7d6d32992877e71772e8f536c --- /dev/null +++ b/src/test/resources/datasets/dataset-add.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/datasets/dataset-delete.xml b/src/test/resources/datasets/dataset-delete.xml new file mode 100644 index 0000000000000000000000000000000000000000..e49223db335c0f49f7d78cdcb3f27e80e14d30da --- /dev/null +++ b/src/test/resources/datasets/dataset-delete.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/datasets/dataset-modify.xml b/src/test/resources/datasets/dataset-modify.xml new file mode 100644 index 0000000000000000000000000000000000000000..6e2dfc903798c9e26431fbaf42bea176a05fc59c --- /dev/null +++ b/src/test/resources/datasets/dataset-modify.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/datasets/dataset.dtd b/src/test/resources/datasets/dataset.dtd new file mode 100644 index 0000000000000000000000000000000000000000..e64500f9760310832c4352657d6b41454a653582 --- /dev/null +++ b/src/test/resources/datasets/dataset.dtd @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/test/resources/datasets/dataset.xml b/src/test/resources/datasets/dataset.xml new file mode 100644 index 0000000000000000000000000000000000000000..3f48cc9b6888070b9fb5b4c42bee31f8eae267b6 --- /dev/null +++ b/src/test/resources/datasets/dataset.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/db/hsqldb-drop.sql b/src/test/resources/db/hsqldb-drop.sql new file mode 100644 index 0000000000000000000000000000000000000000..31f86431b16948308e44087e73b488fc3043628a --- /dev/null +++ b/src/test/resources/db/hsqldb-drop.sql @@ -0,0 +1,2 @@ +DROP TABLE People IF EXISTS; +DROP TABLE Users IF EXISTS; diff --git a/src/test/resources/db/hsqldb.sql b/src/test/resources/db/hsqldb.sql new file mode 100644 index 0000000000000000000000000000000000000000..a62944149d65c732b38a85ca9455f804265fd185 --- /dev/null +++ b/src/test/resources/db/hsqldb.sql @@ -0,0 +1,13 @@ +CREATE TABLE people ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL, + name VARCHAR(50) NOT NULL, + surname VARCHAR(100) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE users ( + login VARCHAR(100) NOT NULL, + password VARCHAR(64) NOT NULL, + role VARCHAR(5) NOT NULL, + PRIMARY KEY (login) +); \ No newline at end of file diff --git a/src/test/webapp/META-INF/context.xml b/src/test/webapp/META-INF/context.xml new file mode 100644 index 0000000000000000000000000000000000000000..dbba264d1c4141c9fe643ecd89e1e82e68525c9b --- /dev/null +++ b/src/test/webapp/META-INF/context.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/webapp/rest/people/add.html b/src/test/webapp/rest/people/add.html new file mode 100644 index 0000000000000000000000000000000000000000..c0f7b7df36c086b33d5cd3d060db6152f17887ba --- /dev/null +++ b/src/test/webapp/rest/people/add.html @@ -0,0 +1,132 @@ + + + + + + +add + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
add
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodyname=Xián&surname=Ximénez
clicklink=POST
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value200 OK
waitForElementPresentcss=#response-body-raw > pre
storeTextcss=#response-body-raw > preresponseBody
echo${responseBody}
assertEvalJSON.parse(storedVars['responseBody']).nameXián
assertEvalJSON.parse(storedVars['responseBody']).surnameXiménez
+ + diff --git a/src/test/webapp/rest/people/addNoName.html b/src/test/webapp/rest/people/addNoName.html new file mode 100644 index 0000000000000000000000000000000000000000..04db2e9b02a105d248647e90b47ee2941f1e00ed --- /dev/null +++ b/src/test/webapp/rest/people/addNoName.html @@ -0,0 +1,107 @@ + + + + + + +addNoName + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
addNoName
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodysurname=Ximénez
clicklink=POST
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value400 Bad Request
+ + diff --git a/src/test/webapp/rest/people/addNoSurname.html b/src/test/webapp/rest/people/addNoSurname.html new file mode 100644 index 0000000000000000000000000000000000000000..6b7ebbf457f46d0d2729fde6e24854bd71b0fbcc --- /dev/null +++ b/src/test/webapp/rest/people/addNoSurname.html @@ -0,0 +1,107 @@ + + + + + + +addNoSurname + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
addNoSurname
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodyname=Xián
clicklink=POST
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value400 Bad Request
+ + diff --git a/src/test/webapp/rest/people/delete.html b/src/test/webapp/rest/people/delete.html new file mode 100644 index 0000000000000000000000000000000000000000..7c1a242b86336b296ec9932772a4709c89b561ea --- /dev/null +++ b/src/test/webapp/rest/people/delete.html @@ -0,0 +1,77 @@ + + + + + + +delete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
delete
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=DELETE
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people/11
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value200 OK
+ + diff --git a/src/test/webapp/rest/people/deleteInvalidId.html b/src/test/webapp/rest/people/deleteInvalidId.html new file mode 100644 index 0000000000000000000000000000000000000000..8d84af44137d53cb60eede774fbc650abf35c1f6 --- /dev/null +++ b/src/test/webapp/rest/people/deleteInvalidId.html @@ -0,0 +1,77 @@ + + + + + + +deleteInvalidId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
deleteInvalidId
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=DELETE
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people/100
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value400 Bad Request
+ + diff --git a/src/test/webapp/rest/people/get.html b/src/test/webapp/rest/people/get.html new file mode 100644 index 0000000000000000000000000000000000000000..30f59e5d9748fa97f4c955125f54d0fda99a2d68 --- /dev/null +++ b/src/test/webapp/rest/people/get.html @@ -0,0 +1,96 @@ + + + + + + +rest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
rest
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=GET
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value200 OK
waitForElementPresentcss=#response-body-raw > pre
storeTextcss=#response-body-raw > preresponseBody
echo${responseBody}
assertEvalJSON.parse(storedVars['responseBody']).length10
+ + diff --git a/src/test/webapp/rest/people/list.html b/src/test/webapp/rest/people/list.html new file mode 100644 index 0000000000000000000000000000000000000000..30f59e5d9748fa97f4c955125f54d0fda99a2d68 --- /dev/null +++ b/src/test/webapp/rest/people/list.html @@ -0,0 +1,96 @@ + + + + + + +rest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
rest
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=GET
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value200 OK
waitForElementPresentcss=#response-body-raw > pre
storeTextcss=#response-body-raw > preresponseBody
echo${responseBody}
assertEvalJSON.parse(storedVars['responseBody']).length10
+ + diff --git a/src/test/webapp/rest/people/modify.html b/src/test/webapp/rest/people/modify.html new file mode 100644 index 0000000000000000000000000000000000000000..0adc9f2a68a806af53478c1e425f7814d7812497 --- /dev/null +++ b/src/test/webapp/rest/people/modify.html @@ -0,0 +1,132 @@ + + + + + + +modify + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
modify
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodyname=Marta&surname=Martínez
clicklink=PUT
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people/4
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value200 OK
waitForElementPresentcss=#response-body-raw > pre
storeTextcss=#response-body-raw > preresponseBody
echo${responseBody}
assertEvalJSON.parse(storedVars['responseBody']).nameMarta
assertEvalJSON.parse(storedVars['responseBody']).surnameMartínez
+ + diff --git a/src/test/webapp/rest/people/modifyInvalidId.html b/src/test/webapp/rest/people/modifyInvalidId.html new file mode 100644 index 0000000000000000000000000000000000000000..3f9d78c28d04964bce35fde1623916c4c1f3655a --- /dev/null +++ b/src/test/webapp/rest/people/modifyInvalidId.html @@ -0,0 +1,107 @@ + + + + + + +modifyInvalidId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
modifyInvalidId
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodyname=Marta&surname=Martínez
clicklink=PUT
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people/100
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value400 Bad Request
+ + diff --git a/src/test/webapp/rest/people/modifyNoId.html b/src/test/webapp/rest/people/modifyNoId.html new file mode 100644 index 0000000000000000000000000000000000000000..ed0f3bbb0db98543dd3aae94f5d4100f4ac612ec --- /dev/null +++ b/src/test/webapp/rest/people/modifyNoId.html @@ -0,0 +1,107 @@ + + + + + + +modifyNoId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
modifyNoId
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodyname=Marta&surname=Martínez
clicklink=PUT
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value405 Method Not Allowed
+ + diff --git a/src/test/webapp/rest/people/modifyNoName.html b/src/test/webapp/rest/people/modifyNoName.html new file mode 100644 index 0000000000000000000000000000000000000000..c29cf81617684baf34bd828194075ec07260d55e --- /dev/null +++ b/src/test/webapp/rest/people/modifyNoName.html @@ -0,0 +1,107 @@ + + + + + + +modifyNoName + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
modifyNoName
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodysurname=Martínez
clicklink=PUT
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people/4
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value400 Bad Request
+ + diff --git a/src/test/webapp/rest/people/modifyNoSurname.html b/src/test/webapp/rest/people/modifyNoSurname.html new file mode 100644 index 0000000000000000000000000000000000000000..fa2f859971853cf66f6827191ca94bf03ace29b7 --- /dev/null +++ b/src/test/webapp/rest/people/modifyNoSurname.html @@ -0,0 +1,107 @@ + + + + + + +modifyNoSurname + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
modifyNoSurname
openchrome://restclient/content/restclient.html
clicklink=Headers
clicklink=Custom Header
typename=nameCookie
typename=valuetoken=bXJqYXRvOm1yamF0bw==
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
clicklink=Headers
clicklink=Custom Header
typename=nameContent-Type
typename=valueapplication/x-www-form-urlencoded
clickcss=#modal-custom-header > div.modal-footer > input.btn.btn-inverse
typeid=request-bodyname=Marta
clicklink=PUT
typeid=request-urlhttp://localhost:9080/DAAExample/rest/people/4
clickid=request-button
clicklink=×
waitForElementPresentcss=span.header-value
assertTextcss=span.header-value400 Bad Request
+ + diff --git a/src/test/webapp/rest/people/rest.html b/src/test/webapp/rest/people/rest.html new file mode 100644 index 0000000000000000000000000000000000000000..4dd4a50ba84da579259318519e76ea9673e32f56 --- /dev/null +++ b/src/test/webapp/rest/people/rest.html @@ -0,0 +1,24 @@ + + + + + + Test Suite + + + + + + + + + + + + + + + +
Test Suite
list
add
addNoName
addNoSurname
modify
modifyInvalidId
modifyNoId
modifyNoName
modifyNoSurname
delete
deleteInvalidId
+ + diff --git a/src/test/webapp/web/people/add.html b/src/test/webapp/web/people/add.html new file mode 100644 index 0000000000000000000000000000000000000000..13d4ad63479307e6b37542ee2678fa1c2bdb7bde --- /dev/null +++ b/src/test/webapp/web/people/add.html @@ -0,0 +1,71 @@ + + + + + + +example + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
example
createCookietoken=bXJqYXRvOm1yamF0bw==
openmain.html
waitForPageToLoad
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
typename=nameHola
typename=surnameMundo
clickid=btnSubmit
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
verifyTextcss=tr:last-child > td.nameHola
verifyTextcss=tr:last-child > td.surnameMundo
deleteCookietoken
+ + diff --git a/src/test/webapp/web/people/delete.html b/src/test/webapp/web/people/delete.html new file mode 100644 index 0000000000000000000000000000000000000000..d73f24795fac92ee915f93ccebc5e7a240d04a60 --- /dev/null +++ b/src/test/webapp/web/people/delete.html @@ -0,0 +1,76 @@ + + + + + + +delete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
delete
createCookietoken=bXJqYXRvOm1yamF0bw==
openmain.html
waitForPageToLoad
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
storeXpathCount//trrows
clickxpath=(//a[contains(text(),'Delete')])[last()]
assertConfirmationEstá a punto de eliminar a una persona. ¿Está seguro de que desea continuar?
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
storeXpathCount//trrowsAfterDeletion
storeEvalstoredVars['rows']-storedVars['rowsAfterDeletion']rowsDeleted
verifyExpression${rowsDeleted}1
deleteCookietoken
+ + diff --git a/src/test/webapp/web/people/edit.html b/src/test/webapp/web/people/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..30688fbe039ee31600c88882d8d086abbfcd0e9c --- /dev/null +++ b/src/test/webapp/web/people/edit.html @@ -0,0 +1,82 @@ + + + + + + +edit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
edit
createCookietoken=bXJqYXRvOm1yamF0bw==
openmain.html
waitForPageToLoad
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
clickxpath=(//a[contains(text(),'Edit')])[last()]
storeAttribute//tr[last()]/@idpersonId
typename=nameAna
typename=surnameMaría
clickid=btnSubmit
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
verifyText//tr[@id='${personId}']/td[@class = 'name']Ana
verifyText//tr[@id='${personId}']/td[@class = 'surname']María
deleteCookietoken
+ + diff --git a/src/test/webapp/web/people/example.html b/src/test/webapp/web/people/example.html new file mode 100644 index 0000000000000000000000000000000000000000..e1dbb63c7c06f84fb2fdef1066c88da329189bdd --- /dev/null +++ b/src/test/webapp/web/people/example.html @@ -0,0 +1,17 @@ + + + + + + Test Suite + + + + + + + + +
Test Suite
list
add
edit
delete
+ + diff --git a/src/test/webapp/web/people/list.html b/src/test/webapp/web/people/list.html new file mode 100644 index 0000000000000000000000000000000000000000..a6b2ac41a9d402ec5e8a93d8b40242ce5c6b073b --- /dev/null +++ b/src/test/webapp/web/people/list.html @@ -0,0 +1,46 @@ + + + + + + +list + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
list
createCookietoken=bXJqYXRvOm1yamF0bw==
openmain.html
waitForPageToLoad
waitForConditionselenium.browserbot.getCurrentWindow().jQuery.active == 01000
verifyXpathCount//tr11
deleteCookietoken
+ + diff --git a/tomcat/server.hsqldb.xml b/tomcat/server.hsqldb.xml new file mode 100644 index 0000000000000000000000000000000000000000..64c6e65b145665c0d694beb459e33f253d52b0a7 --- /dev/null +++ b/tomcat/server.hsqldb.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tomcat/server.mysql.xml b/tomcat/server.mysql.xml new file mode 100644 index 0000000000000000000000000000000000000000..e9b8849d9241961ced5189d47667cc5abc481871 --- /dev/null +++ b/tomcat/server.mysql.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file