diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..3c0d21b5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.classpath +.project +.settings +target + +# Intellij +.idea/ +*.iml +*.iws +workspace.xml +coverage-error.log + +# Vagrant +.vagrant/ +oracle-xe-11.2.0-1.0.x86_64.rpm.zip diff --git a/LICENSE-GPLv3.txt b/LICENSE-GPLv3.txt new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/LICENSE-GPLv3.txt @@ -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. + + + Copyright (C) + + 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: + + Copyright (C) + 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/README.txt b/README.txt new file mode 100644 index 000000000..99012b790 --- /dev/null +++ b/README.txt @@ -0,0 +1,209 @@ +README +###### + +ABOUT +##### + +Lavagna is a small and easy to use agile issue/project tracking software. + +It require : java 7 or better, mysql (5.1 or better) or postgresql. It can be deployed in a java servlet container. + + +INSTALL +####### + +Lavagna support mysql (at least 5.1) / pgsql for production use and hsqldb for testing purpose. + +It's distributed in 2 forms: + + - simple war for deploying in your preferred web container + - self contained war with embedded jetty web server + +For trial purpose +----------------- + +If you want to test it locally, you can download the self contained war and run (java 7 or greater required): + +>java -Ddatasource.driver=org.hsqldb.jdbcDriver -Ddatasource.dialect=HSQLDB -Ddatasource.url=jdbc:hsqldb:mem:lavagna -Ddatasource.username=sa -Ddatasource.password= -Dspring.profile.active=dev -jar lavagna-jetty-console.war --headless + +Go to http://localhost:8080 and login with "user" (password "user"). + +Setup +----- + +Lavagna require the following property to be set on the jvm: + + - datasource.driver=org.hsqldb.jdbcDriver | com.mysql.jdbc.Driver | org.postgresql.Driver + - datasource.dialect=HSQLDB | MYSQL | PGSQL + - datasource.url= for example: jdbc:hsqldb:mem:lavagna | jdbc:mysql://localhost:3306/lavagna | jdbc:postgresql://localhost:5432/lavagna + - datasource.username= + - datasource.password= + - spring.profile.active= dev | prod + +The db user must be able to create tables and others db objects. + +Once the application has been started/deployed, go to + +http(s)://(:port)/setup/ + +There you can: + +1: configure the application +2: import a lavagna export + +Configuration steps +------------------- + +step 1: define the base url +step 2: define the initial login configuration (demo, ldap, oauth, mozilla persona) +step 3: define the admin user +step 4: confirm + + +DEVELOP +####### + +IDE Configuration +================= + +This project use project lombok annotations, you will need to install the support in your IDE. + +Use UTF-8 encoding. + + +Execute +======= + +launch web server: + +# mvn jetty:run + +for launching web server + db manager (hsqldb only) + +# mvn jetty:run -DstartDBManager + +for launching web server with the mysql database (use mysql profile): + +# mvn jetty:run -Pdev-mysql + +# mvn jetty:run -Pdev-pgsql + +- go to http://localhost:8080 + if you have a 403 error, you must configure the application, + go to http://localhost:8080/setup, select demo + insert user "user". + +- enter + username: user + password: user + +For debugging + +# mvndebug jetty:run + +For running the test cases + +# mvn test + +For running the test cases with mysql or pgsql + +# mvn test -Ddatasource.dialect=MYSQL + +# mvn test -Ddatasource.dialect=PGSQL + + +For running with jetty-runner: + +# mvn clean install +# java -Ddatasource.dialect=HSQLDB -Ddatasource.driver=org.hsqldb.jdbcDriver -Ddatasource.url=jdbc:hsqldb:mem:lavagna -Ddatasource.username=sa -Ddatasource.password= -Dspring.profiles.active=dev -jar target/dependency/jetty-runner.jar --port 8080 target/*.war + +VAGRANT +============= + +Make sure that you have installed Vagrant and VirtualBox. + +Initialization +-------------- + +Fetch the submodules before: + +# git submodule update --init + +If you are under windows you need to ensure that the pgsql submodule is not in a broken state, +ensure that the file puppet\modules\postgresql\files\validate_postgresql_connection.sh is using the +unix end of line (run dos2unix). + +To run the tests with Vagrant boot the VMs with + +# vagrant up [optionally use pgsql / mysql to boot only one VM] + +Once that the VM is up and running run the tests: + +# mvn test -Ddatasource.dialect=PGSQL / MYSQL + + +Connecting manually: +-------------------- + +PGSQL: localhost:5432/lavagna as postgres / password + +MySQL: localhost:3306/lavagna as root + +Oracle: localhost:1521/XE as system / manager + +Notes about databases: +---------------------- + +The application use UTF-8 at every stage, on mysql you will need to create a database with the collation set to utf8_bin : + +CREATE DATABASE lavagna CHARACTER SET utf8 COLLATE utf8_bin; + + + + +Oracle support: +--------------- + +First add the vbguest plugin: + +# vagrant plugin install vagrant-vbguest + +Note: if you have an error while installing the vagrant-vbguest plugin, see https://github.com/WinRb/vagrant-windows/issues/193 , install before the vagrant-login plugin with + +# vagrant plugin install vagrant-login + + +Download Oracle Database 11g Express Edition for Linux x64 from ( http://www.oracle.com/technetwork/database/database-technologies/express-edition/downloads/index.html ) + +Place the file oracle-xe-11.2.0-1.0.x86_64.rpm.zip in the directory puppet/modules/oracle/files of this project. + +Thanks to Hilverd Reker for his GitHub repo: https://github.com/hilverd/vagrant-ubuntu-oracle-xe . + + + +CODE COVERAGE +============= + +Jacoco plugin is used. + +# mvn install site + +-> open target/site/jacoco/index.html with your browser + +DATABASE MIGRATION +================== + +Can be disabled using the following system property: datasource.disable.migration=true + + +CHECK FOR UPDATED DEPENDENCIES +============================== + +Note: + +- hsqldb atm will not be updated to version 2.3.2 due to a bug + (default null+unique clause has changed) +- tomcat-jdbc will not be updated to version 8.0.9 due to a strange + class loader interaction with log4j when launching with mvn jetty:run + +mvn versions:display-dependency-updates +mvn versions:display-plugin-updates diff --git a/README_EXECUTABLE_WAR.txt b/README_EXECUTABLE_WAR.txt new file mode 100644 index 000000000..835ab55cc --- /dev/null +++ b/README_EXECUTABLE_WAR.txt @@ -0,0 +1,17 @@ +A second war named lavagna-jetty-console.war will be built. Inside there is an embedded jetty. + +You must provide the following properties: + +- datasource.driver=org.hsqldb.jdbcDriver | com.mysql.jdbc.Driver | org.postgresql.Driver +- datasource.dialect=HSQLDB | MYSQL | PGSQL +- datasource.url= for example: jdbc:hsqldb:mem:lavagna | jdbc:mysql://localhost:3306/lavagna | jdbc:postgresql://localhost:5432/lavagna +- datasource.username= +- datasource.password= +- spring.profile.active= dev | prod + +For example: + +>java -Ddatasource.driver=org.hsqldb.jdbcDriver -Ddatasource.dialect=HSQLDB -Ddatasource.url=jdbc:hsqldb:mem:lavagna -Ddatasource.username=sa -Ddatasource.password= -Dspring.profile.active=dev -jar lavagna-jetty-console.war --headless + + +You can set port and others options too, see: http://simplericity.com/2009/11/10/1257880778509.html \ No newline at end of file diff --git a/README_OPENSHIFT_HEROKU.txt b/README_OPENSHIFT_HEROKU.txt new file mode 100644 index 000000000..2c8515d60 --- /dev/null +++ b/README_OPENSHIFT_HEROKU.txt @@ -0,0 +1,26 @@ +Openshift: + +using this cartridge +http://cartreflect-claytondev.rhcloud.com/reflect?github=Worldline/openshift-cartridge-jetty-websocket + +https://github.com/worldline/openshift-cartridge-jetty-websocket + + + +Heroku: + +Relevant urls: + + - https://devcenter.heroku.com/articles/deploy-a-java-web-application-that-launches-with-jetty-runner + - https://devcenter.heroku.com/articles/connecting-to-relational-databases-on-heroku-with-java + - https://devcenter.heroku.com/articles/heroku-labs-websockets + +Procfile content: +-------------------- +web: java $JAVA_OPTS -Ddatasource.dialect=PGSQL -Ddatasource.driver=org.postgresql.Driver -Ddatasource.url=$DATABASE_URL -Dspring.profiles.active=prod -jar target/dependency/jetty-runner.jar --port $PORT target/*.war +-------------------- + +system.properties content: +-------------------- +java.runtime.version=1.7 +-------------------- \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..044029bc1 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,63 @@ +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + + config.vm.define 'pgsql' do |pgsql| + pgsql.vm.box = 'precise64' + pgsql.vm.box_url = "http://files.vagrantup.com/precise64.box" + + pgsql.vm.network 'forwarded_port', guest: 5432, host: 5432 + + pgsql.vm.provision 'puppet' do |puppet| + puppet.module_path = 'puppet/modules' + puppet.manifests_path = 'puppet/manifests' + puppet.manifest_file = 'pgsql.pp' + puppet.facter = { + 'db' => 'lavagna' + } + end + + end + + config.vm.define 'mysql' do |mysql| + mysql.vm.box = 'precise64' + mysql.vm.box_url = "http://files.vagrantup.com/precise64.box" + + mysql.vm.network "forwarded_port", guest: 3306, host: 3306 + + mysql.vm.provision "puppet" do |puppet| + puppet.module_path = 'puppet/modules' + puppet.manifests_path = 'puppet/manifests' + puppet.manifest_file = 'mysql.pp' + puppet.facter = { + 'db' => 'lavagna' + } + end + + end + + config.vm.define 'oracle' do |oracle| + oracle.vm.box = 'precise64' + oracle.vm.box_url = "http://files.vagrantup.com/precise64.box" + oracle.vm.hostname = "oracle" + + oracle.vm.network :forwarded_port, guest: 1521, host: 1521 + + oracle.vm.provider :virtualbox do |vb| + vb.customize ["modifyvm", :id, + "--name", "oracle", + "--memory", "512", + "--natdnshostresolver1", "on"] + end + + oracle.vm.provision :shell, :inline => "echo \"America/New_York\" | sudo tee /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata" + + oracle.vm.provision :puppet do |puppet| + puppet.manifests_path = "puppet/manifests" + puppet.module_path = "puppet/modules" + puppet.manifest_file = "oracle.pp" + puppet.options = "--verbose --trace" + end + + end +end \ No newline at end of file diff --git a/misc/NOTES.txt b/misc/NOTES.txt new file mode 100644 index 000000000..b26403ec0 --- /dev/null +++ b/misc/NOTES.txt @@ -0,0 +1,18 @@ +REST API return data + +GET: +- success: data / failure: exception + +DELETE: +- success: 1/0 / failure: exception + +ADD (no duplicates): +- success: nothing / failure: exception + +ADD (duplicates): +- success: IDs / failure: exception + +UPDATE: +- success: nothing / failure: exception + +this will affect the return types of the services/controllers. \ No newline at end of file diff --git a/misc/OAUTH_LOGIN.txt b/misc/OAUTH_LOGIN.txt new file mode 100644 index 000000000..c402a6111 --- /dev/null +++ b/misc/OAUTH_LOGIN.txt @@ -0,0 +1,4 @@ +For google, you should enable the following apis: + +- Google+ API +- Google+ Domains API \ No newline at end of file diff --git a/misc/RESOURCES.txt b/misc/RESOURCES.txt new file mode 100644 index 000000000..8a32065fa --- /dev/null +++ b/misc/RESOURCES.txt @@ -0,0 +1,49 @@ +route, try to follow the RoR convention: + +http://guides.rubyonrails.org/routing.html#crud-verbs-and-actions (# 2.2 CRUD, Verbs, and Actions ) + + +angular is taken from + +https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.js +https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular-sanitize.js + + +notes: +- http://blog.smartbear.com/open-source/how-to-turn-your-pile-of-code-into-an-open-source-project/ + +markdown parser: + +- https://github.com/chjj/marked + +hihglight + +- http://highlightjs.org/ + +elastic text area + + - http://monospaced.github.io/angular-elastic/ + https://github.com/monospaced/angular-elastic + https://github.com/monospaced/angular-elastic/releases v2.0.0 + + + + sockjs: + + - taken from https://raw.github.com/rstoyanchev/spring-websocket-portfolio/master/src/main/webapp/assets/lib/sockjs/sockjs.min.js + + stomp: + + - taken from https://raw.github.com/rstoyanchev/spring-websocket-portfolio/master/src/main/webapp/assets/lib/stomp/dist/stomp.min.js + + spectrum: https://rawgithub.com/bgrins/spectrum/master/spectrum.css + js + + - taken from + + how to center stuff with bootstrap: + http://stackoverflow.com/a/18153551 + + + codemirror: + + - core + markdown + placeholder v3.21 @ http://codemirror.net/doc/compress.html \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..ef1cc46b7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,505 @@ + + 4.0.0 + io.lavagna + lavagna + war + 1.0-SNAPSHOT + lavagna + http://maven.apache.org + + + + org.springframework + spring-webmvc + ${org.springframework.version} + + + org.springframework + spring-websocket + ${org.springframework.version} + + + org.springframework + spring-messaging + ${org.springframework.version} + + + org.springframework + spring-jdbc + ${org.springframework.version} + + + org.springframework + spring-context-support + ${org.springframework.version} + + + + org.springframework + spring-test + ${org.springframework.version} + test + + + + org.apache.commons + commons-lang3 + 3.3.2 + + + commons-codec + commons-codec + 1.10 + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.17 + test + + + org.kubek2k + springockito + 1.0.9 + test + + + + com.google.code.gson + gson + 2.3.1 + + + + org.flywaydb + flyway-core + 3.1 + + + + org.scribe + scribe + 1.3.7 + + + + + org.apache.logging.log4j + log4j-api + ${org.apache.logging.log4j.version} + + + org.apache.logging.log4j + log4j-core + ${org.apache.logging.log4j.version} + + + org.apache.logging.log4j + log4j-jcl + ${org.apache.logging.log4j.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${org.apache.logging.log4j.version} + + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + + com.julienvey.trello + trello-java-wrapper + 0.3 + + + + logback-core + ch.qos.logback + + + logback-classic + ch.qos.logback + + + + + + + com.samskivert + jmustache + 1.9 + + + + org.hsqldb + hsqldb + 2.3.1 + + + mysql + mysql-connector-java + 5.1.34 + + + org.postgresql + postgresql + 9.3-1102-jdbc41 + + + org.apache.tomcat + tomcat-jdbc + 8.0.8 + + + + org.tuckey + urlrewritefilter + 4.0.4 + + + javax.mail + mail + 1.4.6 + + + + org.projectlombok + lombok + 1.14.8 + provided + + + + + + + 2.1 + 4.1.3.RELEASE + 9.2.6.v20141205 + UTF-8 + src/main + + + + lavagna + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + UTF-8 + + + + org.apache.maven.plugins + maven-war-plugin + 2.4 + + false + + + + org.eclipse.jetty + jetty-maven-plugin + ${org.eclipse.jetty.version} + + + + ${jetty.port} + ${jetty.host} + + + + spring.profiles.active + ${spring.profiles.active} + + + datasource.dialect + ${datasource.dialect} + + + datasource.driver + ${datasource.driver} + + + datasource.url + ${datasource.url} + + + datasource.username + ${datasource.username} + + + datasource.password + ${datasource.password} + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.9 + + + package + + copy + + + + + org.eclipse.jetty + jetty-runner + ${org.eclipse.jetty.version} + jetty-runner.jar + + + + + + + + + org.simplericity.jettyconsole + jetty-console-maven-plugin + 1.55 + + + + createconsole + + + + + + + + com.mycila + license-maven-plugin + 2.6 + +
com/mycila/maven/plugin/license/templates/GPL-3.txt
+ + **/*.java + **/*.sql + **/common.properties + **/MYSQL.properties + **/PGSQL.properties + +
+ + + + check + + + +
+ + org.jacoco + jacoco-maven-plugin + 0.7.2.201409121644 + + + + prepare-agent + + + + report + prepare-package + + report + + + + +
+ + + src/main/resources/ + false + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.jacoco + + jacoco-maven-plugin + + + [0.7.0.201403182114,) + + + prepare-agent + + + + + + + + + + + + +
+ + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.2 + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.0 + + + org.codehaus.mojo + jdepend-maven-plugin + 2.0 + + + + + + + dev + + true + + + 8080 + 0.0.0.0 + HSQLDB + + + org.hsqldb.jdbcDriver + jdbc:hsqldb:mem:lavagna + sa + + dev + + + + dev-c9 + + ${env.PORT} + ${env.IP} + HSQLDB + + + org.hsqldb.jdbcDriver + jdbc:hsqldb:mem:lavagna + sa + + + dev + + + + dev-mysql + + 8080 + 0.0.0.0 + MYSQL + + + com.mysql.jdbc.Driver + jdbc:mysql://localhost:3306/lavagna + root + + dev + + + + dev-pgsql + + 8080 + 0.0.0.0 + PGSQL + org.postgresql.Driver + jdbc:postgresql://localhost:5432/lavagna + postgres + password + dev + + + + openshift + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + ${env.OPENSHIFT_JETTY_PORT} + ${env.OPENSHIFT_JETTY_IP} + MYSQL + + + com.mysql.jdbc.Driver + jdbc:mysql://${env.OPENSHIFT_MYSQL_DB_HOST}:${env.OPENSHIFT_MYSQL_DB_PORT}/lavagna + ${env.OPENSHIFT_MYSQL_DB_USERNAME} + ${env.OPENSHIFT_MYSQL_DB_PASSWORD} + prod + + + + +
diff --git a/puppet/manifests/mysql.pp b/puppet/manifests/mysql.pp new file mode 100644 index 000000000..4188ee4a1 --- /dev/null +++ b/puppet/manifests/mysql.pp @@ -0,0 +1,35 @@ +class { 'apt': + always_apt_update => true +} +-> +class { 'mysql::server': + override_options => { 'mysqld' => { 'bind-address' => '0.0.0.0' } } +} +-> +class { 'mysql::client': } +-> +mysql_database { $db: + ensure => 'present', + charset => 'utf8', + collate => 'utf8_bin', +} +-> +mysql_user { 'root@%': + ensure => 'present', + max_connections_per_hour => '0', + max_queries_per_hour => '0', + max_updates_per_hour => '0', + max_user_connections => '0', +} +-> +mysql_grant { 'root@%/*.*': + ensure => 'present', + options => ['GRANT'], + privileges => ['ALL'], + table => '*.*', + user => 'root@%', +} +-> +exec { 'restart_mysql': + command => '/usr/sbin/service mysql restart' +} \ No newline at end of file diff --git a/puppet/manifests/oracle.pp b/puppet/manifests/oracle.pp new file mode 100644 index 000000000..877984f06 --- /dev/null +++ b/puppet/manifests/oracle.pp @@ -0,0 +1,11 @@ +node oracle { + include oracle::server + include oracle::swap + include oracle::xe + + user { "vagrant": + groups => "dba", + # So that we let Oracle installer create the group + require => Service["oracle-xe"], + } +} \ No newline at end of file diff --git a/puppet/manifests/pgsql.pp b/puppet/manifests/pgsql.pp new file mode 100644 index 000000000..a9ccd3a0b --- /dev/null +++ b/puppet/manifests/pgsql.pp @@ -0,0 +1,30 @@ +class {'postgresql::globals': + manage_package_repo => true, + encoding => 'UTF8', + locale => 'en_US.utf8' +}-> +class {'postgresql::server': + ensure => 'present', + listen_addresses => '*', + ipv4acls => ['host all all 0.0.0.0/0 md5'], + ip_mask_deny_postgres_user => '0.0.0.0/32', + ip_mask_allow_all_users => '0.0.0.0/0' +} + +postgresql::server::role { 'postgres': + password_hash => postgresql_password('postgres', 'password'), + superuser => true, + login => true, + createdb => true +} + +postgresql::server::db { $db: + user => 'postgres', + password => postgresql_password('postgres', 'password'), + encoding => 'UTF8', + locale => 'en_US.utf8' +} + +class { 'postgresql::server::contrib': + package_ensure => 'present', +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/common/Bind.java b/src/main/java/io/lavagna/common/Bind.java new file mode 100644 index 000000000..61fb32db5 --- /dev/null +++ b/src/main/java/io/lavagna/common/Bind.java @@ -0,0 +1,28 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Bind { + String value(); +} diff --git a/src/main/java/io/lavagna/common/ConstructorAnnotationRowMapper.java b/src/main/java/io/lavagna/common/ConstructorAnnotationRowMapper.java new file mode 100644 index 000000000..8b000c395 --- /dev/null +++ b/src/main/java/io/lavagna/common/ConstructorAnnotationRowMapper.java @@ -0,0 +1,230 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.sql.Clob; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.Assert; + +public class ConstructorAnnotationRowMapper implements RowMapper { + + private final Constructor con; + private final List mappedColumn; + + /** + * Check if the given class has the correct form. + *

+ *

    + *
  • must have exactly one public constructor.
  • + *
  • must at least have one parameter.
  • + *
  • all the parameters must be annotated with @Column annotation.
  • + *
+ * + * @param clazz + * @return + */ + public static boolean hasConstructorInTheCorrectForm(Class clazz) { + + if (clazz.getConstructors().length != 1) { + return false; + } + + Constructor con = clazz.getConstructors()[0]; + + if (con.getParameterTypes().length == 0) { + return false; + } + + Annotation[][] parameterAnnotations = con.getParameterAnnotations(); + for (Annotation[] as : parameterAnnotations) { + if (!hasColumnAnnotation(as)) { + return false; + } + } + + return true; + } + + private static boolean hasColumnAnnotation(Annotation[] as) { + if (as == null || as.length == 0) { + return false; + } + for (Annotation a : as) { + if (a.annotationType().isAssignableFrom(Column.class)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("unchecked") + public ConstructorAnnotationRowMapper(Class clazz) { + int constructorCount = clazz.getConstructors().length; + Assert.isTrue(constructorCount == 1, "The class " + clazz.getName() + + " must have exactly one public constructor, " + constructorCount + " are present"); + + con = (Constructor) clazz.getConstructors()[0]; + mappedColumn = from(clazz, con.getParameterAnnotations(), con.getParameterTypes()); + } + + @Override + public T mapRow(ResultSet rs, int rowNum) throws SQLException { + List vals = new ArrayList<>(mappedColumn.size()); + + for (ColumnMapper colMapper : mappedColumn) { + vals.add(colMapper.getObject(rs)); + } + + try { + return con.newInstance(vals.toArray(new Object[vals.size()])); + } catch (ReflectiveOperationException e) { + throw new SQLException(e); + } catch (IllegalArgumentException e) { + throw new SQLException("type mismatch between the expected one from the construct and the one passed," + + " check 1: some values are null and passed to primitive types 2: incompatible numeric types", e); + } + } + + private static List from(Class clazz, Annotation[][] annotations, Class[] paramTypes) { + List res = new ArrayList<>(); + for (int i = 0; i < annotations.length; i++) { + res.add(findColumnAnnotationValue(clazz, i, annotations[i], paramTypes[i])); + } + return res; + } + + private static ColumnMapper findColumnAnnotationValue(Class clazz, int position, Annotation[] annotations, + Class paramType) { + + for (Annotation a : annotations) { + if (Column.class.isAssignableFrom(a.annotationType())) { + + String name = ((Column) a).value(); + + if (paramType.isEnum()) { + return new EnumColumnMapper(name, paramType); + } else if (boolean.class == paramType || Boolean.class == paramType) { + return new BooleanColumnMapper(name); + } else { + return new ColumnMapper(name); + } + } + } + + throw new IllegalStateException("No annotation @Column found for class: " + clazz.getName() + + " in constructor at position " + position); + } + + static class ColumnMapper { + protected final String name; + + ColumnMapper(String name) { + this.name = name; + } + + public Object getObject(ResultSet rs) throws SQLException { + Object res = rs.getObject(name); + if (res != null && Clob.class.isAssignableFrom(res.getClass())) { + try (ClobAutoCloseable clob = new ClobAutoCloseable((Clob) res)) { + return clob.clob.getSubString(1, (int) clob.clob.length()); + } + } else if (res != null && BigDecimal.class.isAssignableFrom(res.getClass())) { + return ((BigDecimal) res).longValue(); + } else { + return res; + } + } + } + + private static class ClobAutoCloseable implements AutoCloseable { + + private final Clob clob; + + public ClobAutoCloseable(Clob clob) { + this.clob = clob; + } + + @Override + public void close() throws SQLException { + clob.free(); + } + } + + static class BooleanColumnMapper extends ColumnMapper { + + BooleanColumnMapper(String name) { + super(name); + } + + public Object getObject(ResultSet rs) throws SQLException { + Object res = rs.getObject(name); + Class resClass = res == null ? null : res.getClass(); + if (res == null || Boolean.class.isAssignableFrom(resClass)) { + return res; + } else if (Number.class.isAssignableFrom(resClass)) { + return 1 == ((Number) res).intValue(); + } else if (String.class.isAssignableFrom(resClass)) { + return "true".equalsIgnoreCase(res.toString()); + } else { + throw new IllegalArgumentException("was not able to extract a boolean value"); + } + } + } + + static class EnumColumnMapper extends ColumnMapper { + + @SuppressWarnings("rawtypes") + private final Class enumType; + + @SuppressWarnings("unchecked") EnumColumnMapper(String name, Class enumType) { + super(name); + this.enumType = (Class>) enumType; + } + + @SuppressWarnings("unchecked") + @Override + public Object getObject(ResultSet rs) throws SQLException { + String res = rs.getString(name); + return res == null ? null : Enum.valueOf(enumType, res); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface Column { + /** + * Column name + * + * @return + */ + String value(); + } + +} diff --git a/src/main/java/io/lavagna/common/DatabaseMigrationDoneEvent.java b/src/main/java/io/lavagna/common/DatabaseMigrationDoneEvent.java new file mode 100644 index 000000000..95a11f30c --- /dev/null +++ b/src/main/java/io/lavagna/common/DatabaseMigrationDoneEvent.java @@ -0,0 +1,28 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import org.springframework.context.ApplicationEvent; + +public class DatabaseMigrationDoneEvent extends ApplicationEvent { + + private static final long serialVersionUID = 4359436382324341606L; + + public DatabaseMigrationDoneEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/io/lavagna/common/Json.java b/src/main/java/io/lavagna/common/Json.java new file mode 100644 index 000000000..5fd653b06 --- /dev/null +++ b/src/main/java/io/lavagna/common/Json.java @@ -0,0 +1,29 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public final class Json { + + private Json() { + } + + public static final Gson GSON = new GsonBuilder().serializeNulls().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .create(); +} diff --git a/src/main/java/io/lavagna/common/QueriesOverride.java b/src/main/java/io/lavagna/common/QueriesOverride.java new file mode 100644 index 000000000..6d4be6554 --- /dev/null +++ b/src/main/java/io/lavagna/common/QueriesOverride.java @@ -0,0 +1,28 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface QueriesOverride { + QueryOverride[] value(); +} diff --git a/src/main/java/io/lavagna/common/Query.java b/src/main/java/io/lavagna/common/Query.java new file mode 100644 index 000000000..79974f711 --- /dev/null +++ b/src/main/java/io/lavagna/common/Query.java @@ -0,0 +1,30 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Query { + String value(); + + QueryType type() default QueryType.EXECUTE; +} diff --git a/src/main/java/io/lavagna/common/QueryFactory.java b/src/main/java/io/lavagna/common/QueryFactory.java new file mode 100644 index 000000000..9c29ed53a --- /dev/null +++ b/src/main/java/io/lavagna/common/QueryFactory.java @@ -0,0 +1,87 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.util.Assert; + +public class QueryFactory { + + private final String activeDb; + private final NamedParameterJdbcTemplate jdbc; + + public QueryFactory(String activeDB, NamedParameterJdbcTemplate jdbc) { + this.activeDb = activeDB; + this.jdbc = jdbc; + } + + public static T from(final Class clazz, final String activeDb) { + return from(clazz, activeDb, null); + } + + private static class QueryTypeAndQuery { + private final QueryType type; + private final String query; + + QueryTypeAndQuery(QueryType type, String query) { + this.type = type; + this.query = query; + } + } + + @SuppressWarnings("unchecked") + private static T from(final Class clazz, final String activeDb, final NamedParameterJdbcTemplate jdbc) { + return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + QueryTypeAndQuery qs = extractQueryAnnotation(clazz, activeDb, method); + return qs.type.apply(qs.query, jdbc, method, args); + } + }); + } + + private static QueryTypeAndQuery extractQueryAnnotation(Class clazz, String activeDb, Method method) { + Query q = method.getAnnotation(Query.class); + QueriesOverride qs = method.getAnnotation(QueriesOverride.class); + + Assert.isTrue( + q != null, + String.format("missing @Query annotation for method %s in interface %s", method.getName(), + clazz.getSimpleName())); + // only one @Query annotation, thus we return the value without checking the database + if (qs == null) { + return new QueryTypeAndQuery(q.type(), q.value()); + } + + for (QueryOverride query : qs.value()) { + if (query.db().equals(activeDb)) { + return new QueryTypeAndQuery(q.type(), query.value()); + } + } + + return new QueryTypeAndQuery(q.type(), q.value()); + } + + public T from(final Class clazz) { + return from(clazz, activeDb, jdbc); + } + +} diff --git a/src/main/java/io/lavagna/common/QueryOverride.java b/src/main/java/io/lavagna/common/QueryOverride.java new file mode 100644 index 000000000..814bea84c --- /dev/null +++ b/src/main/java/io/lavagna/common/QueryOverride.java @@ -0,0 +1,30 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface QueryOverride { + String value(); + + String db(); +} diff --git a/src/main/java/io/lavagna/common/QueryRepository.java b/src/main/java/io/lavagna/common/QueryRepository.java new file mode 100644 index 000000000..7b654ff53 --- /dev/null +++ b/src/main/java/io/lavagna/common/QueryRepository.java @@ -0,0 +1,28 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface QueryRepository { + +} diff --git a/src/main/java/io/lavagna/common/QueryRepositoryScanner.java b/src/main/java/io/lavagna/common/QueryRepositoryScanner.java new file mode 100644 index 000000000..0a460b789 --- /dev/null +++ b/src/main/java/io/lavagna/common/QueryRepositoryScanner.java @@ -0,0 +1,74 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; + +public class QueryRepositoryScanner implements BeanFactoryPostProcessor { + + private final String[] packagesToScan; + private final QueryFactory queryFactory; + + public QueryRepositoryScanner(QueryFactory queryFactory, String... packagesToScan) { + this.queryFactory = queryFactory; + this.packagesToScan = packagesToScan; + } + + private static class CustomClasspathScanner extends ClassPathScanningCandidateComponentProvider { + + public CustomClasspathScanner() { + super(false); + addIncludeFilter(new AnnotationTypeFilter(QueryRepository.class, false)); + } + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return beanDefinition.getMetadata().isInterface(); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (packagesToScan != null) { + CustomClasspathScanner scanner = new CustomClasspathScanner(); + for (String packageToScan : packagesToScan) { + Set candidates = scanner.findCandidateComponents(packageToScan); + handleCandidates(candidates, beanFactory); + } + } + } + + private void handleCandidates(Set candidates, final ConfigurableListableBeanFactory beanFactory) { + try { + for (BeanDefinition beanDefinition : candidates) { + final Class c = Class.forName(beanDefinition.getBeanClassName()); + beanFactory.registerSingleton(beanDefinition.getBeanClassName(), queryFactory.from(c)); + } + } catch (ClassNotFoundException cnf) { + throw new IllegalStateException("Error while loading class", cnf); + } + } + +} diff --git a/src/main/java/io/lavagna/common/QueryType.java b/src/main/java/io/lavagna/common/QueryType.java new file mode 100644 index 000000000..40d15ca1b --- /dev/null +++ b/src/main/java/io/lavagna/common/QueryType.java @@ -0,0 +1,172 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.util.StringUtils; + +/** + * Query Type: + * + *
    + *
  • TEMPLATE : we receive the string defined in @Query/@QueryOverride annotation. + *
  • EXECUTE : the query will be executed. If it's a select, the result will be mapped with a + * ConstructorAnnotationRowMapper if it has the correct form. + *
+ * + */ +public enum QueryType { + /** + * Receive the string defined in @Query/@QueryOverride annotation. + */ + TEMPLATE { + @Override + Object apply(String template, NamedParameterJdbcTemplate jdbc, Method method, Object[] args) { + return template; + } + }, + + /** + */ + EXECUTE { + + /** + * Keep a mapping between a given class and a possible RowMapper. + * + * If the Class has the correct form, a ConstructorAnnotationRowMapper will be built and the boolean set to true + * in the pair. If the class has not the correct form, the boolean will be false and the class will be used as + * it is in the jdbc template. + */ + private final Map, HasRowmapper> cachedClassToMapper = new ConcurrentHashMap, HasRowmapper>(); + + @Override + Object apply(String template, NamedParameterJdbcTemplate jdbc, Method method, Object[] args) { + JdbcAction action = actionFromTemplate(template); + SqlParameterSource parameters = extractParameters(method, args); + if (action == JdbcAction.QUERY) { + return doQuery(template, jdbc, method, parameters); + } else { + return jdbc.update(template, parameters); + } + } + + @SuppressWarnings("unchecked") + private Object doQuery(String template, NamedParameterJdbcTemplate jdbc, Method method, + SqlParameterSource parameters) { + if (method.getReturnType().isAssignableFrom(List.class)) { + Class c = (Class) ((ParameterizedType) method.getGenericReturnType()) + .getActualTypeArguments()[0]; + HasRowmapper r = ensurePresence(c); + if (r.present) { + return jdbc.query(template, parameters, r.rowMapper); + } else { + return jdbc.queryForList(template, parameters, c); + } + } else { + Class c = (Class) method.getReturnType(); + HasRowmapper r = ensurePresence(c); + if (r.present) { + return jdbc.queryForObject(template, parameters, r.rowMapper); + } else { + return jdbc.queryForObject(template, parameters, c); + } + } + } + + private HasRowmapper ensurePresence(Class c) { + if (!cachedClassToMapper.containsKey(c)) { + cachedClassToMapper.put(c, handleClass(c)); + } + return cachedClassToMapper.get(c); + } + }; + + private static JdbcAction actionFromTemplate(String template) { + String tmpl = StringUtils.deleteAny(template.toLowerCase(Locale.ENGLISH), "() ").trim(); + return tmpl.indexOf("select") == 0 ? JdbcAction.QUERY : JdbcAction.UPDATE; + } + + private enum JdbcAction { + QUERY, UPDATE + } + + abstract Object apply(String template, NamedParameterJdbcTemplate jdbc, Method method, Object[] args); + + private static HasRowmapper handleClass(Class c) { + if (ConstructorAnnotationRowMapper.hasConstructorInTheCorrectForm(c)) { + return new HasRowmapper(true, new ConstructorAnnotationRowMapper(c)); + } else { + return new HasRowmapper(false, null); + } + } + + private static SqlParameterSource extractParameters(Method m, Object[] args) { + + Class[] parameterTypes = m.getParameterTypes(); + Annotation[][] parameterAnnotations = m.getParameterAnnotations(); + if (parameterAnnotations == null || parameterAnnotations.length == 0) { + return new EmptySqlParameterSource(); + } + + MapSqlParameterSource ps = new MapSqlParameterSource(); + for (int i = 0; i < args.length; i++) { + String name = parameterName(parameterAnnotations[i]); + if (name != null) { + ps.addValue(name, args[i], StatementCreatorUtils.javaTypeToSqlParameterType(parameterTypes[i])); + } + } + + return ps; + } + + private static String parameterName(Annotation[] annotation) { + + if (annotation == null) { + return null; + } + + for (Annotation a : annotation) { + if (a instanceof Bind) { + return ((Bind) a).value(); + } + } + return null; + } + + private static class HasRowmapper { + private final boolean present; + private final RowMapper rowMapper; + + HasRowmapper(boolean present, RowMapper rowMapper) { + this.present = present; + this.rowMapper = rowMapper; + } + } +} diff --git a/src/main/java/io/lavagna/common/Read.java b/src/main/java/io/lavagna/common/Read.java new file mode 100644 index 000000000..a8d19dec8 --- /dev/null +++ b/src/main/java/io/lavagna/common/Read.java @@ -0,0 +1,72 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.common; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import com.google.gson.reflect.TypeToken; + +public class Read { + + public static Path readFile(String name, Path tempFile) throws IOException { + try (InputStream is = Files.newInputStream(tempFile); ZipInputStream zis = new ZipInputStream(is)) { + ZipEntry ze = zis.getNextEntry(); + while (ze != null) { + if (ze.getName().equals(name)) { + Path p = Files.createTempFile(null, null); + Files.copy(zis, p, StandardCopyOption.REPLACE_EXISTING); + return p; + } + ze = zis.getNextEntry(); + } + } + return null; + } + + public static T readObject(String name, Path tempFile, TypeToken t) { + return readMatchingObjects(name, tempFile, t).get(0); + } + + @SuppressWarnings("unchecked") + public static List readMatchingObjects(String regex, Path tempFile, TypeToken t) { + try { + List res = new ArrayList<>(); + try (InputStream is = Files.newInputStream(tempFile); ZipInputStream zis = new ZipInputStream(is)) { + ZipEntry ze = zis.getNextEntry(); + while (ze != null) { + if (ze.getName().matches("^" + regex + "$")) { + res.add((T) Json.GSON.fromJson(new InputStreamReader(zis, StandardCharsets.UTF_8), t.getType())); + } + ze = zis.getNextEntry(); + } + return res; + } + } catch (IOException ioe) { + throw new IllegalStateException("error while reading data for " + regex, ioe); + } + } +} diff --git a/src/main/java/io/lavagna/config/DataSourceConfig.java b/src/main/java/io/lavagna/config/DataSourceConfig.java new file mode 100644 index 000000000..65c3ad045 --- /dev/null +++ b/src/main/java/io/lavagna/config/DataSourceConfig.java @@ -0,0 +1,107 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.config; + +import io.lavagna.common.DatabaseMigrationDoneEvent; +import io.lavagna.common.QueryFactory; +import io.lavagna.query.ValidationQuery; +import io.lavagna.service.DatabaseMigrator; + +import java.net.URI; +import java.net.URISyntaxException; + +import javax.sql.DataSource; + +import org.apache.commons.lang3.ArrayUtils; +import org.flywaydb.core.api.MigrationVersion; +import org.hsqldb.util.DatabaseManagerSwing; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +public class DataSourceConfig { + + public static final MigrationVersion LATEST_STABLE_VERSION = MigrationVersion.fromVersion("1"); + + @Bean(destroyMethod = "close") + public DataSource getDataSource(Environment env) throws URISyntaxException { + org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); + dataSource.setDriverClassName(env.getRequiredProperty("datasource.driver")); + + if (env.containsProperty("datasource.url") && // + env.containsProperty("datasource.username") && // + env.containsProperty("datasource.password")) { + urlAndCredentials(dataSource, env); + } else { + urlWithCredentials(dataSource, env); + } + + dataSource.setValidationQuery(QueryFactory.from(ValidationQuery.class, + env.getRequiredProperty("datasource.dialect")).validation()); + dataSource.setTestOnBorrow(true); + dataSource.setTestOnConnect(true); + dataSource.setTestWhileIdle(true); + + + if (System.getProperty("startDBManager") != null) { + DatabaseManagerSwing.main(new String[] { "--url", "jdbc:hsqldb:mem:lavagna", "--noexit" }); + } + + return dataSource; + } + + + /** + * for supporting heroku style url: + * + *
+	 * [database type]://[username]:[password]@[host]:[port]/[database name]
+	 * 
+ * + * @param dataSource + * @param env + * @throws URISyntaxException + */ + private static void urlWithCredentials(org.apache.tomcat.jdbc.pool.DataSource dataSource, Environment env) + throws URISyntaxException { + URI dbUri = new URI(env.getRequiredProperty("datasource.url")); + dataSource.setUsername(dbUri.getUserInfo().split(":")[0]); + dataSource.setPassword(dbUri.getUserInfo().split(":")[1]); + dataSource.setUrl(String.format("%s://%s:%s%s", scheme(dbUri), dbUri.getHost(), dbUri.getPort(), + dbUri.getPath())); + } + + private static String scheme(URI uri) { + return "postgres".equals(uri.getScheme()) ? "jdbc:postgresql" : uri.getScheme(); + } + + private static void urlAndCredentials(org.apache.tomcat.jdbc.pool.DataSource dataSource, Environment env) { + dataSource.setUrl(env.getRequiredProperty("datasource.url")); + dataSource.setUsername(env.getRequiredProperty("datasource.username")); + dataSource.setPassword(env.getRequiredProperty("datasource.password")); + } + + @Bean + public DatabaseMigrator migrator(Environment env, DataSource dataSource, ApplicationEventPublisher publisher) { + + boolean isDev = ArrayUtils.contains(env.getActiveProfiles(),"dev"); + + DatabaseMigrator migrator = new DatabaseMigrator(env, dataSource, isDev ? MigrationVersion.LATEST : LATEST_STABLE_VERSION); + publisher.publishEvent(new DatabaseMigrationDoneEvent(this)); + return migrator; + } +} diff --git a/src/main/java/io/lavagna/config/DispatcherServletInitializer.java b/src/main/java/io/lavagna/config/DispatcherServletInitializer.java new file mode 100644 index 000000000..0528d0346 --- /dev/null +++ b/src/main/java/io/lavagna/config/DispatcherServletInitializer.java @@ -0,0 +1,85 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.config; + +import io.lavagna.web.security.SecurityFilter; + +import java.util.Collections; + +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration.Dynamic; +import javax.servlet.SessionTrackingMode; + +import org.springframework.web.filter.ShallowEtagHeaderFilter; +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; +import org.tuckey.web.filters.urlrewrite.gzip.GzipFilter; + +public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return new Class[] { DataSourceConfig.class,// + PersistenceAndServiceConfig.class,// + WebSecurityConfig.class}; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { WebConfig.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + super.onStartup(servletContext); + + javax.servlet.FilterRegistration.Dynamic secFilter = servletContext.addFilter("SecurityFilter", + SecurityFilter.class); + secFilter.setAsyncSupported(true); + secFilter.addMappingForUrlPatterns(null, false, "/*"); + + javax.servlet.FilterRegistration.Dynamic etagFilter = servletContext.addFilter("ETagFilter", + ShallowEtagHeaderFilter.class); + etagFilter.addMappingForUrlPatterns(null, false, "*.js", "*.css",// + "/", "/project/*", "/admin/*", "/me/",// + "*.html", "*.woff", "*.eot", "*.svg", "*.ttf"); + + javax.servlet.FilterRegistration.Dynamic gzipFilter = servletContext.addFilter("GzipFilter", GzipFilter.class); + gzipFilter.addMappingForUrlPatterns(null, false, "*.js", "*.css",// + "/", "/project/*", "/admin/*", "/me/",// + "/api/self", "/api/board/*", "/api/project/*"); + servletContext.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE)); + servletContext.getSessionCookieConfig().setHttpOnly(true); + servletContext.getSessionCookieConfig().setName("LAVAGNA_SESSION_ID"); + } + + @Override + protected void customizeRegistration(Dynamic registration) { + + MultipartConfigElement multipartConfigElement = new MultipartConfigElement(""); + + registration.setMultipartConfig(multipartConfigElement); + registration.setInitParameter("dispatchOptionsRequest", "true"); + registration.setAsyncSupported(true); + } +} diff --git a/src/main/java/io/lavagna/config/PersistenceAndServiceConfig.java b/src/main/java/io/lavagna/config/PersistenceAndServiceConfig.java new file mode 100644 index 000000000..d24c8f3f8 --- /dev/null +++ b/src/main/java/io/lavagna/config/PersistenceAndServiceConfig.java @@ -0,0 +1,141 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.config; + +import io.lavagna.common.QueryFactory; +import io.lavagna.common.QueryRepositoryScanner; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.logging.log4j.LogManager; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.core.env.Environment; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.util.ErrorHandler; +import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; + +/** + * Datasource configuration. + */ +@EnableScheduling +@EnableWebSocketMessageBroker +@EnableTransactionManagement +@ComponentScan(basePackages = { "io.lavagna.service", "io.lavagna.config.dbmanager" }) +public class PersistenceAndServiceConfig extends AbstractWebSocketMessageBrokerConfigurer implements + SchedulingConfigurer { + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskScheduler()); + } + + @Bean + public PlatformTransactionManager platformTransactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + private static class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { + protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) { + // because mysql don't support check constraints :( + if (sqlex.getMessage().contains("RAISE_CHECK_ERROR")) { + return new DataIntegrityViolationException(task, sqlex); + } + return null; + } + } + + @Bean + public NamedParameterJdbcTemplate simpleJdbcTemplate(Environment env, DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + // mysql does not support check constraints + if ("MYSQL".equals(env.getProperty("datasource.dialect"))) { + CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator(); + tr.setDataSource(dataSource); + jdbcTemplate.setExceptionTranslator(tr); + } + return new NamedParameterJdbcTemplate(jdbcTemplate); + } + + @Bean + public QueryRepositoryScanner queryRepoScanner(QueryFactory queryFactory) { + return new QueryRepositoryScanner(queryFactory, "io.lavagna.query"); + } + + @Bean + public QueryFactory queryFactory(Environment env, NamedParameterJdbcTemplate jdbc) { + return new QueryFactory(env.getProperty("datasource.dialect"), jdbc); + } + + @Bean + public LobHandler lobHander() { + return new DefaultLobHandler(); + } + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource(); + source.setBasename("classpath:/io/lavagna/i18n/messages"); + source.setUseCodeAsDefaultMessage(true); + source.setFallbackToSystemLocale(false); + return source; + } + + @Bean(destroyMethod = "shutdown") + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setErrorHandler(new ErrorHandler() { + @Override + public void handleError(Throwable t) { + LogManager.getLogger().error("error while handling job", t); + } + }); + scheduler.initialize(); + return scheduler; + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/api/socket").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/event"); + } +} diff --git a/src/main/java/io/lavagna/config/WebConfig.java b/src/main/java/io/lavagna/config/WebConfig.java new file mode 100644 index 000000000..e65ad2f5d --- /dev/null +++ b/src/main/java/io/lavagna/config/WebConfig.java @@ -0,0 +1,85 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.config; + +import io.lavagna.web.helper.GeneralHandlerExceptionResolver; +import io.lavagna.web.helper.GsonHttpMessageConverter; +import io.lavagna.web.helper.PermissionMethodInterceptor; +import io.lavagna.web.helper.UserArgumentResolver; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.multipart.support.StandardServletMultipartResolver; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +@EnableWebMvc +@ComponentScan(basePackages = "io.lavagna.web") +// scan only the web controller, the rest is done statically +public class WebConfig extends WebMvcConfigurerAdapter { + + @Autowired + private UserArgumentResolver userArgumentResolver; + + @Autowired + private PermissionMethodInterceptor permissionMethodInterceptor; + + // enable serving static files through default servlet + + public void configurePathMatch(org.springframework.web.servlet.config.annotation.PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(false); + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(new GsonHttpMessageConverter()); + } + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(permissionMethodInterceptor); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(userArgumentResolver); + } + + @Override + public void configureHandlerExceptionResolvers(List exceptionResolvers) { + exceptionResolvers.add(new GeneralHandlerExceptionResolver()); + } + + @Bean + public MultipartResolver multipartResolver() { + return new StandardServletMultipartResolver(); + } +} diff --git a/src/main/java/io/lavagna/config/WebSecurityConfig.java b/src/main/java/io/lavagna/config/WebSecurityConfig.java new file mode 100644 index 000000000..24e70136c --- /dev/null +++ b/src/main/java/io/lavagna/config/WebSecurityConfig.java @@ -0,0 +1,94 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.config; + +import static java.util.Arrays.asList; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.Ldap; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.GsonHttpMessageConverter; +import io.lavagna.web.security.PathConfiguration; +import io.lavagna.web.security.login.DemoLogin; +import io.lavagna.web.security.login.LdapLogin; +import io.lavagna.web.security.login.OAuthLogin; +import io.lavagna.web.security.login.PersonaLogin; +import io.lavagna.web.security.login.OAuthLogin.Handler; + +import org.scribe.builder.ServiceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +public class WebSecurityConfig { + + @Bean(name = "configuredAppPathConf") + public PathConfiguration configuredApp() { + return new PathConfiguration()// + .request("/favicon.ico").permitAll()// + .request("/css/all.css").permitAll()// + .request("/setup/**").denyAll()// + .request("/api/**").requireAuthenticated(false)// + .request("/**").requireAuthenticated()// + .login("/login/**", "/login", "/WEB-INF/views/login.html").logout("/logout/**", "/logout"); + } + + @Bean(name = "unconfiguredAppPathConf") + public PathConfiguration unconfiguredApp() { + return new PathConfiguration().request("/setup/**").permitAll()// + .request("/bootstrap-3.0/**").permitAll()// + .request("/css/**").permitAll()// + .request("/js/**").permitAll()// + .request("/**").denyAll().disableLogin(); + } + + @Lazy + @Bean + public DemoLogin demoLogin(UserRepository userRepository) { + return new DemoLogin(userRepository, "/login?error-demo"); + } + + @Lazy + @Bean + public OAuthLogin oauthLogin(UserRepository userRepository, ConfigurationRepository configurationRepository) { + return new OAuthLogin(userRepository, configurationRepository, new Handler(new ServiceBuilder()), + "/login?error-oauth"); + } + + @Lazy + @Bean + public LdapLogin ldapLogin(UserRepository userRepository, ConfigurationRepository configurationRepository, Ldap ldap) { + return new LdapLogin(userRepository, ldap, "/login?error-ldap"); + } + + @Lazy + @Bean + public PersonaLogin personaLogin(UserRepository userRepository, ConfigurationRepository configurationRepository, + RestTemplate restTemplate) { + return new PersonaLogin(userRepository, configurationRepository, restTemplate, + "/WEB-INF/views/logout-persona.html"); + } + + @Lazy + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setMessageConverters(asList(new FormHttpMessageConverter(), new GsonHttpMessageConverter())); + return restTemplate; + } + +} diff --git a/src/main/java/io/lavagna/model/Board.java b/src/main/java/io/lavagna/model/Board.java new file mode 100644 index 000000000..63b26f288 --- /dev/null +++ b/src/main/java/io/lavagna/model/Board.java @@ -0,0 +1,42 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class Board { + + private final int id; + private final String name; + private final String shortName; + private final String description; + private final int projectId; + private final boolean archived; + + public Board(@Column("BOARD_ID") int id, @Column("BOARD_NAME") String name, + @Column("BOARD_SHORT_NAME") String shortName, @Column("BOARD_DESCRIPTION") String description, + @Column("BOARD_PROJECT_ID_FK") int projectId, @Column("BOARD_ARCHIVED") boolean archived) { + this.id = id; + this.name = name; + this.shortName = shortName; + this.description = description; + this.projectId = projectId; + this.archived = archived; + } +} diff --git a/src/main/java/io/lavagna/model/BoardColumn.java b/src/main/java/io/lavagna/model/BoardColumn.java new file mode 100644 index 000000000..e005ff774 --- /dev/null +++ b/src/main/java/io/lavagna/model/BoardColumn.java @@ -0,0 +1,74 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.Event.EventType; + +@Getter +public class BoardColumn { + + public enum BoardColumnLocation { + + BOARD, BACKLOG, ARCHIVE, TRASH; + + public static final Map MAPPING; + + static { + Map mapping = new EnumMap<>(BoardColumnLocation.class); + mapping.put(BoardColumnLocation.ARCHIVE, EventType.CARD_ARCHIVE); + mapping.put(BoardColumnLocation.BACKLOG, EventType.CARD_BACKLOG); + mapping.put(BoardColumnLocation.TRASH, EventType.CARD_TRASH); + mapping.put(BoardColumnLocation.BOARD, EventType.CARD_CREATE); + MAPPING = Collections.unmodifiableMap(mapping); + } + } + + private final int id; + private final String name; + private final int order; + private final int boardId; + private final BoardColumnLocation location; + private final int definitionId; + private final ColumnDefinition status; + private final int color; + + public BoardColumn( + @Column("BOARD_COLUMN_ID") int id,// + @Column("BOARD_COLUMN_NAME") String name,// + @Column("BOARD_COLUMN_ORDER") int order,// + @Column("BOARD_COLUMN_BOARD_ID_FK") int boardId,// + @Column("BOARD_COLUMN_LOCATION") BoardColumnLocation location, + @Column("BOARD_COLUMN_DEFINITION_ID") int definitionId, + @Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition status, + @Column("BOARD_COLUMN_DEFINITION_COLOR") int color) { + this.id = id; + this.name = name; + this.order = order; + this.boardId = boardId; + this.location = location; + + this.definitionId = definitionId; + this.status = status; + this.color = color; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/BoardColumnDefinition.java b/src/main/java/io/lavagna/model/BoardColumnDefinition.java new file mode 100644 index 000000000..97c7e0761 --- /dev/null +++ b/src/main/java/io/lavagna/model/BoardColumnDefinition.java @@ -0,0 +1,39 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class BoardColumnDefinition { + + private final int id; + private final int projectId; + private final ColumnDefinition value; + private final int color; + + public BoardColumnDefinition(@Column("BOARD_COLUMN_DEFINITION_ID") int id, + @Column("BOARD_COLUMN_DEFINITION_PROJECT_ID_FK") int projectId, + @Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition value, + @Column("BOARD_COLUMN_DEFINITION_COLOR") int color) { + this.id = id; + this.projectId = projectId; + this.value = value; + this.color = color; + } +} diff --git a/src/main/java/io/lavagna/model/BoardColumnInfo.java b/src/main/java/io/lavagna/model/BoardColumnInfo.java new file mode 100644 index 000000000..4fe6661a9 --- /dev/null +++ b/src/main/java/io/lavagna/model/BoardColumnInfo.java @@ -0,0 +1,56 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.BoardColumn.BoardColumnLocation; + +@Getter +public class BoardColumnInfo { + + private final int columnId; + private final String columnName; + private final ColumnDefinition columnDefinition; + private final BoardColumnLocation columnLocation; + private final int columnColor; + private final int boardId; + private final String boardName; + private final String boardShortName; + private final int projectId; + private final String projectName; + + public BoardColumnInfo(@Column("BOARD_COLUMN_ID") int columnId, @Column("BOARD_COLUMN_NAME") String columnName, + @Column("BOARD_COLUMN_LOCATION") BoardColumnLocation columnLocation, @Column("BOARD_ID") int boardId, + @Column("BOARD_NAME") String boardName, @Column("BOARD_SHORT_NAME") String boardShortName, + @Column("PROJECT_ID") int projectId, @Column("PROJECT_NAME") String projectName, + @Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition columnDefininition, + @Column("BOARD_COLUMN_DEFINITION_COLOR") int columnColor) { + this.columnId = columnId; + this.columnName = columnName; + this.columnDefinition = columnDefininition; + this.columnColor = columnColor; + this.columnLocation = columnLocation; + + this.boardId = boardId; + this.boardName = boardName; + this.boardShortName = boardShortName; + + this.projectId = projectId; + this.projectName = projectName; + } +} diff --git a/src/main/java/io/lavagna/model/BoardInfo.java b/src/main/java/io/lavagna/model/BoardInfo.java new file mode 100644 index 000000000..fa0ac4e69 --- /dev/null +++ b/src/main/java/io/lavagna/model/BoardInfo.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import lombok.Getter; + +@Getter +public class BoardInfo { + private final String shortName; + private final String name; + private final String description; + private final boolean archived; + + public BoardInfo(@Column("BOARD_SHORT_NAME") String shortName, @Column("BOARD_NAME") String name, + @Column("BOARD_DESCRIPTION") String description, @Column("BOARD_ARCHIVED") boolean archived) { + this.shortName = shortName; + this.name = name; + this.description = description; + this.archived = archived; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/Card.java b/src/main/java/io/lavagna/model/Card.java new file mode 100644 index 000000000..427bb4135 --- /dev/null +++ b/src/main/java/io/lavagna/model/Card.java @@ -0,0 +1,45 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class Card { + + private final int id; + private final String name; + private final int sequence;// sequence number, public identifier + private final int order; + private final int columnId; + private final int userId; + + public Card(@Column("CARD_ID") int id,// + @Column("CARD_NAME") String name,// + @Column("CARD_SEQ_NUMBER") int sequence,// + @Column("CARD_ORDER") int order,// + @Column("CARD_BOARD_COLUMN_ID_FK") int columnId,// + @Column("CARD_USER_ID_FK") int userId) { + this.id = id; + this.name = name; + this.sequence = sequence; + this.order = order; + this.columnId = columnId; + this.userId = userId; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/CardData.java b/src/main/java/io/lavagna/model/CardData.java new file mode 100644 index 000000000..5cf43688c --- /dev/null +++ b/src/main/java/io/lavagna/model/CardData.java @@ -0,0 +1,33 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class CardData extends CardDataMetadata { + + private final String content; + + public CardData(@Column("CARD_DATA_ID") int id, @Column("CARD_DATA_CARD_ID_FK") int cardId, + @Column("CARD_DATA_REFERENCE_ID") Integer referenceId, @Column("CARD_DATA_TYPE") CardType type, + @Column("CARD_DATA_CONTENT") String content, @Column("CARD_DATA_ORDER") int order) { + super(id, cardId, referenceId, type, order); + this.content = content; + } +} diff --git a/src/main/java/io/lavagna/model/CardDataCount.java b/src/main/java/io/lavagna/model/CardDataCount.java new file mode 100644 index 000000000..ad9017040 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardDataCount.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class CardDataCount { + + private final int cardId; + private final String type; + private final Number count; + + public CardDataCount(@Column("CARD_ID") int cardId,// + @Column("CARD_DATA_TYPE") String type,// + @Column("CARD_DATA_TYPE_COUNT") Number count) { + this.cardId = cardId; + this.type = type; + this.count = count; + } +} diff --git a/src/main/java/io/lavagna/model/CardDataFull.java b/src/main/java/io/lavagna/model/CardDataFull.java new file mode 100644 index 000000000..09f624a4b --- /dev/null +++ b/src/main/java/io/lavagna/model/CardDataFull.java @@ -0,0 +1,55 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.Event.EventType; + +import java.util.Date; + +import lombok.Getter; + +@Getter +public class CardDataFull { + private final int id; + private final Integer referenceId; + private final int cardId; + private final String content; + private final int userId; + private final Date time; + private final CardType type; + private final EventType eventType; + private final int eventReferenceId; + private final int order; + + public CardDataFull(@Column("CARD_DATA_ID") int id, @Column("CARD_DATA_REFERENCE_ID") Integer referenceId, + @Column("CARD_DATA_CARD_ID_FK") int cardId, @Column("CARD_DATA_CONTENT") String content, + @Column("EVENT_USER_ID_FK") int userId, @Column("EVENT_PREV_CARD_DATA_ID_FK") int eventReferenceId, + @Column("EVENT_TIME") Date time, @Column("CARD_DATA_TYPE") CardType type, + @Column("CARD_DATA_ORDER") int order, @Column("EVENT_TYPE") EventType eventType) { + this.id = id; + this.referenceId = referenceId; + this.cardId = cardId; + this.content = content; + this.userId = userId; + this.time = time; + this.type = type; + this.order = order; + this.eventType = eventType; + this.eventReferenceId = eventReferenceId; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/CardDataIdAndOrder.java b/src/main/java/io/lavagna/model/CardDataIdAndOrder.java new file mode 100644 index 000000000..099cb042e --- /dev/null +++ b/src/main/java/io/lavagna/model/CardDataIdAndOrder.java @@ -0,0 +1,25 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +public class CardDataIdAndOrder extends Pair { + public CardDataIdAndOrder(@Column("CARD_DATA_ID") int first, @Column("CARD_DATA_ORDER") int second) { + super(first, second); + } +} diff --git a/src/main/java/io/lavagna/model/CardDataMetadata.java b/src/main/java/io/lavagna/model/CardDataMetadata.java new file mode 100644 index 000000000..32b974970 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardDataMetadata.java @@ -0,0 +1,40 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class CardDataMetadata { + + private final int id; + private final int cardId; + private final Integer referenceId; + private final CardType type; + private final int order; + + public CardDataMetadata(@Column("CARD_DATA_ID") int id, @Column("CARD_DATA_CARD_ID_FK") int cardId, + @Column("CARD_DATA_REFERENCE_ID") Integer referenceId, @Column("CARD_DATA_TYPE") CardType type, + @Column("CARD_DATA_ORDER") int order) { + this.id = id; + this.cardId = cardId; + this.referenceId = referenceId; + this.type = type; + this.order = order; + } +} diff --git a/src/main/java/io/lavagna/model/CardDataUploadContentInfo.java b/src/main/java/io/lavagna/model/CardDataUploadContentInfo.java new file mode 100644 index 000000000..42b88400b --- /dev/null +++ b/src/main/java/io/lavagna/model/CardDataUploadContentInfo.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class CardDataUploadContentInfo { + + private final String digest; + private final int size; + private final String contentType; + + public CardDataUploadContentInfo(@Column("DIGEST") String digest, @Column("SIZE") int size, + @Column("CONTENT_TYPE") String contentType) { + this.digest = digest; + this.size = size; + this.contentType = contentType; + } + +} diff --git a/src/main/java/io/lavagna/model/CardFull.java b/src/main/java/io/lavagna/model/CardFull.java new file mode 100644 index 000000000..d1e1f78ef --- /dev/null +++ b/src/main/java/io/lavagna/model/CardFull.java @@ -0,0 +1,54 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import java.util.Date; + +import lombok.Getter; + +@Getter +public class CardFull extends Card { + + private final Date createTime; + private final Date lastUpdateTime; + private final int lastUpdateUserId; + private final String projectShortName; + private final String boardShortName; + private final ColumnDefinition columnDefinition; + + public CardFull( + @Column("CARD_ID") int id,// + @Column("CARD_NAME") String name,// + @Column("CARD_SEQ_NUMBER") int sequence,// + @Column("CARD_ORDER") int order,// + @Column("CARD_BOARD_COLUMN_ID_FK") int columnId,// + @Column("CREATE_USER") int createUserId,// + @Column("CREATE_TIME") Date createTime,// + @Column("LAST_UPDATE_USER") int lastUpdateUserId, @Column("LAST_UPDATE_TIME") Date lastUpdateTime, + @Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition columnDefinition, + @Column("BOARD_SHORT_NAME") String boardShortName, @Column("PROJECT_SHORT_NAME") String projectShortName) { + super(id, name, sequence, order, columnId, createUserId); + this.createTime = createTime; + this.lastUpdateTime = lastUpdateTime; + this.lastUpdateUserId = lastUpdateUserId; + this.projectShortName = projectShortName; + this.boardShortName = boardShortName; + this.columnDefinition = columnDefinition; + } +} diff --git a/src/main/java/io/lavagna/model/CardFullWithCounts.java b/src/main/java/io/lavagna/model/CardFullWithCounts.java new file mode 100644 index 000000000..33febd2b5 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardFullWithCounts.java @@ -0,0 +1,140 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +import org.apache.commons.codec.digest.DigestUtils; + +@Getter +public class CardFullWithCounts extends CardFull { + + private final Integer creationUser; + private final Date creationDate; + private final Map counts; + private final List labels; + private final String hash; + + public CardFullWithCounts(CardFull cardInfo, Map counts, List labels) { + super(cardInfo.getId(), cardInfo.getName(), cardInfo.getSequence(), cardInfo.getOrder(), + cardInfo.getColumnId(), cardInfo.getUserId(), cardInfo.getCreateTime(), cardInfo.getLastUpdateUserId(), + cardInfo.getLastUpdateTime(), cardInfo.getColumnDefinition(), cardInfo.getBoardShortName(), cardInfo + .getProjectShortName()); + this.counts = counts; + this.labels = labels == null ? Collections. emptyList() : labels; + // FIXME: this data is already contained in CardFull, leaving it here for retrocompatibility + this.creationUser = cardInfo.getUserId(); + this.creationDate = cardInfo.getCreateTime(); + + hash = hash(this); + } + + private static String hash(CardFullWithCounts cwc) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream daos = new DataOutputStream(baos); + + try { + // card + daos.writeChars(Integer.toString(cwc.getId())); + + writeNotNull(daos, cwc.getName()); + writeInts(daos, cwc.getSequence(), cwc.getOrder(), cwc.getColumnId(), cwc.getUserId()); + // end card + writeNotNull(daos, cwc.creationUser); + writeNotNull(daos, cwc.creationDate); + + if (cwc.counts != null) { + for (Map.Entry count : cwc.counts.entrySet()) { + writeNotNull(daos, count.getKey()); + CardDataCount dataCount = count.getValue(); + daos.writeChars(Integer.toString(dataCount.getCardId())); + if (dataCount.getCount() != null) { + daos.writeChars(Long.toString(dataCount.getCount().longValue())); + } + writeNotNull(daos, dataCount.getType()); + } + } + for (LabelAndValue lv : cwc.labels) { + // + writeInts(daos, lv.getLabelId(), lv.getLabelProjectId()); + writeNotNull(daos, lv.getLabelName()); + daos.writeChars(Integer.toString(lv.getLabelColor())); + writeEnum(daos, lv.getLabelType()); + writeEnum(daos, lv.getLabelDomain()); + // + writeInts(daos, lv.getLabelValueId(), lv.getLabelValueCardId(), lv.getLabelValueLabelId()); + writeNotNull(daos, lv.getLabelValueUseUniqueIndex()); + writeEnum(daos, lv.getLabelValueType()); + writeNotNull(daos, lv.getLabelValueString()); + writeNotNull(daos, lv.getLabelValueTimestamp()); + writeNotNull(daos, lv.getLabelValueInt()); + writeNotNull(daos, lv.getLabelValueCard()); + writeNotNull(daos, lv.getLabelValueUser()); + } + daos.flush(); + + return DigestUtils.sha256Hex(new ByteArrayInputStream(baos.toByteArray())); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static void writeInts(DataOutputStream daos, int... e) throws IOException { + for (int i : e) { + daos.writeChars(Integer.toString(i)); + } + } + + private static void writeNotNull(DataOutputStream daos, Boolean s) throws IOException { + if (s != null) { + daos.writeChars(Boolean.toString(s)); + } + } + + private static void writeNotNull(DataOutputStream daos, Date s) throws IOException { + if (s != null) { + daos.writeChars(Long.toString(s.getTime())); + } + } + + private static void writeNotNull(DataOutputStream daos, String s) throws IOException { + if (s != null) { + daos.writeChars(s); + } + } + + private static void writeNotNull(DataOutputStream daos, Integer val) throws IOException { + if (val != null) { + daos.writeChars(Integer.toString(val)); + } + } + + private static > void writeEnum(DataOutputStream daos, T e) throws IOException { + if (e != null) { + daos.writeChars(e.toString()); + } + } +} diff --git a/src/main/java/io/lavagna/model/CardFullWithCountsHolder.java b/src/main/java/io/lavagna/model/CardFullWithCountsHolder.java new file mode 100644 index 000000000..658fd0d57 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardFullWithCountsHolder.java @@ -0,0 +1,35 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class CardFullWithCountsHolder { + + private final List cards; + private final int totalCards; + private final int itemsPerPage; + + public CardFullWithCountsHolder(List cards, int totalCards, int itemsPerPage) { + this.cards = cards; + this.totalCards = totalCards; + this.itemsPerPage = itemsPerPage; + } +} diff --git a/src/main/java/io/lavagna/model/CardIdAndContent.java b/src/main/java/io/lavagna/model/CardIdAndContent.java new file mode 100644 index 000000000..c4a42e7d0 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardIdAndContent.java @@ -0,0 +1,32 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class CardIdAndContent { + + private final int id; + private final String content; + + public CardIdAndContent(@Column("CARD_DATA_ID") int id, @Column("CARD_DATA_CONTENT") String content) { + this.id = id; + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/CardLabel.java b/src/main/java/io/lavagna/model/CardLabel.java new file mode 100644 index 000000000..699ecd175 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardLabel.java @@ -0,0 +1,93 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +public class CardLabel { + + private final int id; + private final int projectId; + private final String name; + private final int color; + private final boolean unique; + private final LabelType type; + private final LabelDomain domain; + + public static final Set RESERVED_SYSTEM_LABELS_NAME = Collections.unmodifiableSet(new HashSet<>(Arrays + .asList("ASSIGNED", "DUE_DATE", "MILESTONE", "WATCHED_BY"))); + + public CardLabel(@Column("CARD_LABEL_ID") int id, @Column("CARD_LABEL_PROJECT_ID_FK") int projectId, + @Column("CARD_LABEL_UNIQUE") boolean unique, @Column("CARD_LABEL_TYPE") LabelType type, + @Column("CARD_LABEL_DOMAIN") LabelDomain domain, @Column("CARD_LABEL_NAME") String name, + @Column("CARD_LABEL_COLOR") int color) { + this.id = id; + this.projectId = projectId; + this.unique = unique; + this.name = name; + this.color = color; + this.type = type; + this.domain = domain; + } + + public CardLabel name(String newName) { + return new CardLabel(id, projectId, unique, type, domain, newName, color); + } + + public CardLabel color(int newColor) { + return new CardLabel(id, projectId, unique, type, domain, name, newColor); + } + + public CardLabel set(String newName, LabelType newType, int newColor) { + return new CardLabel(id, projectId, unique, newType, domain, newName, newColor); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof CardLabel)) { + return false; + } + CardLabel other = (CardLabel) obj; + return new EqualsBuilder().append(id, other.id).append(projectId, other.projectId).append(unique, other.unique) + .append(type, other.type).append(domain, other.domain).append(name, other.name) + .append(color, other.color).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).append(projectId).append(unique).append(type).append(domain) + .append(name).append(color).toHashCode(); + } + + public enum LabelDomain { + SYSTEM, USER + } + + public enum LabelType { + NULL, STRING, TIMESTAMP, INT, CARD, USER, LIST + } +} diff --git a/src/main/java/io/lavagna/model/CardLabelValue.java b/src/main/java/io/lavagna/model/CardLabelValue.java new file mode 100644 index 000000000..f8af91d19 --- /dev/null +++ b/src/main/java/io/lavagna/model/CardLabelValue.java @@ -0,0 +1,152 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.CardLabel.LabelType; + +import java.util.Date; + +import lombok.Getter; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +public class CardLabelValue { + + private final int cardLabelValueId; + private final int cardId; + private final int labelId; + private final Boolean useUniqueIndex; + private final LabelType labelValueType; + private final LabelValue value; + + public CardLabelValue(@Column("CARD_LABEL_VALUE_ID") int cardLabelValueId, @Column("CARD_ID_FK") int cardId, + @Column("CARD_LABEL_ID_FK") int labelId, + @Column("CARD_LABEL_VALUE_USE_UNIQUE_INDEX") Boolean useUniqueIndex, + @Column("CARD_LABEL_VALUE_TYPE") LabelType labelValueType, + @Column("CARD_LABEL_VALUE_STRING") String valueString, + @Column("CARD_LABEL_VALUE_TIMESTAMP") Date valueDate, @Column("CARD_LABEL_VALUE_INT") Integer valueInt, + @Column("CARD_LABEL_VALUE_CARD_FK") Integer valueCard, + @Column("CARD_LABEL_VALUE_USER_FK") Integer valueUser, + @Column("CARD_LABEL_VALUE_LIST_VALUE_FK") Integer valueList) { + this.cardLabelValueId = cardLabelValueId; + this.cardId = cardId; + this.labelId = labelId; + this.useUniqueIndex = useUniqueIndex; + this.labelValueType = labelValueType; + this.value = new LabelValue(valueString, valueDate, valueInt, valueCard, valueUser, valueList); + } + + public CardLabelValue newValue(String value) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.STRING, value, null, + null, null, null, null); + } + + public CardLabelValue newValue(Date date) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.TIMESTAMP, null, date, + null, null, null, null); + } + + public CardLabelValue newValue(Integer integer) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.INT, null, null, + integer, null, null, null); + } + + public CardLabelValue newCardValue(Integer cardId) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.CARD, null, null, null, + cardId, null, null); + } + + public CardLabelValue newUserValue(Integer userId) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.USER, null, null, null, + null, userId, null); + } + + public CardLabelValue newListValue(Integer listId) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.CARD, null, null, null, + null, null, listId); + } + + public CardLabelValue newNullValue() { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, LabelType.NULL, null, null, null, + null, null, null); + } + + public CardLabelValue newValue(LabelType type, LabelValue value) { + return new CardLabelValue(cardLabelValueId, cardId, labelId, useUniqueIndex, type, value.valueString, + value.valueTimestamp, value.valueInt, value.valueCard, value.valueUser, value.valueList); + } + + @Getter + public static class LabelValue { + private final String valueString; + private final Date valueTimestamp; + private final Integer valueInt; + private final Integer valueCard; + private final Integer valueUser; + private final Integer valueList; + + public LabelValue(@Column("CARD_LABEL_VALUE_STRING") String valueString, + @Column("CARD_LABEL_VALUE_TIMESTAMP") Date valueTimestamp, + @Column("CARD_LABEL_VALUE_INT") Integer valueInt, + @Column("CARD_LABEL_VALUE_CARD_FK") Integer valueCard, + @Column("CARD_LABEL_VALUE_USER_FK") Integer valueUser, + @Column("CARD_LABEL_VALUE_LIST_VALUE_FK") Integer valueList) { + this.valueString = valueString; + this.valueTimestamp = valueTimestamp; + this.valueInt = valueInt; + this.valueCard = valueCard; + this.valueUser = valueUser; + this.valueList = valueList; + } + + public LabelValue(String valueString) { + this(valueString, null, null, null, null, null); + } + + public LabelValue(Date valueTimestamp) { + this(null, valueTimestamp, null, null, null, null); + } + + public LabelValue(Integer valueUser) { + this(null, null, null, null, valueUser, null); + } + + public LabelValue() { + this(null, null, null, null, null, null); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof LabelValue)) { + return false; + } + LabelValue lv = (LabelValue) obj; + return new EqualsBuilder().append(valueString, lv.valueString).append(valueTimestamp, lv.valueTimestamp) + .append(valueInt, lv.valueInt).append(valueCard, lv.valueCard).append(valueUser, lv.valueUser) + .append(valueList, lv.valueList).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(valueString).append(valueTimestamp).append(valueInt).append(valueCard) + .append(valueUser).append(valueList).toHashCode(); + } + } +} diff --git a/src/main/java/io/lavagna/model/CardType.java b/src/main/java/io/lavagna/model/CardType.java new file mode 100644 index 000000000..168c6fcaa --- /dev/null +++ b/src/main/java/io/lavagna/model/CardType.java @@ -0,0 +1,21 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +public enum CardType { + COMMENT, ACTION_LIST, ACTION_CHECKED, ACTION_UNCHECKED, FILE, COMMENT_HISTORY, DESCRIPTION, DESCRIPTION_HISTORY +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/ColumnDefinition.java b/src/main/java/io/lavagna/model/ColumnDefinition.java new file mode 100644 index 000000000..f307f31b9 --- /dev/null +++ b/src/main/java/io/lavagna/model/ColumnDefinition.java @@ -0,0 +1,46 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +public enum ColumnDefinition { + OPEN { + @Override + public int getDefaultColor() { + return 0xd9534f; // red + } + }, + CLOSED { + @Override + public int getDefaultColor() { + return 0x5cb85c; // green + } + }, + BACKLOG { + @Override + public int getDefaultColor() { + return 0x428bca; // blue + } + }, + DEFERRED { + @Override + public int getDefaultColor() { + return 0xf0ad4e; // yellow + } + }; + + public abstract int getDefaultColor(); +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/ConfigurationKeyValue.java b/src/main/java/io/lavagna/model/ConfigurationKeyValue.java new file mode 100644 index 000000000..2c1367f47 --- /dev/null +++ b/src/main/java/io/lavagna/model/ConfigurationKeyValue.java @@ -0,0 +1,25 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +public class ConfigurationKeyValue extends Pair { + public ConfigurationKeyValue(@Column("CONF_KEY") Key first, @Column("CONF_VALUE") String second) { + super(first, second); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/Event.java b/src/main/java/io/lavagna/model/Event.java new file mode 100644 index 000000000..92f2aa47c --- /dev/null +++ b/src/main/java/io/lavagna/model/Event.java @@ -0,0 +1,91 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.CardLabel.LabelType; + +import java.util.Date; + +import lombok.Getter; + +@Getter +public class Event { + + public enum EventType { + CARD_MOVE, CARD_CREATE, CARD_ARCHIVE, CARD_BACKLOG, CARD_TRASH, CARD_UPDATE, + + ACTION_LIST_CREATE, ACTION_LIST_DELETE, ACTION_ITEM_CREATE, ACTION_ITEM_DELETE, ACTION_ITEM_MOVE, ACTION_ITEM_CHECK, ACTION_ITEM_UNCHECK, + + COMMENT_CREATE, COMMENT_UPDATE, COMMENT_DELETE, + + FILE_UPLOAD, FILE_DELETE, + + DESCRIPTION_CREATE, DESCRIPTION_UPDATE, LABEL_CREATE, LABEL_DELETE + } + + private final int id; + private final int cardId; + private final int userId; + private final Date time; + private final EventType event; + private final Integer dataId; + private final Integer columnId; + private final String labelName; + private final LabelType labelType; + private final Integer previousDataId; + private final Integer newDataId; + private final Integer previousColumnId; + private final Integer valueInt; + private final String valueString; + private final Date valueTimestamp; + private final Integer valueCard; + private final Integer valueUser; + + public Event(@Column("EVENT_ID") int id, @Column("EVENT_CARD_ID_FK") int cardId, + @Column("EVENT_USER_ID_FK") int userId, @Column("EVENT_TIME") Date time, + @Column("EVENT_TYPE") EventType event, @Column("EVENT_CARD_DATA_ID_FK") Integer dataId, + @Column("EVENT_COLUMN_ID_FK") Integer columnId, + @Column("EVENT_PREV_CARD_DATA_ID_FK") Integer previousDataId, + @Column("EVENT_NEW_CARD_DATA_ID_FK") Integer newDataId, + @Column("EVENT_PREV_COLUMN_ID_FK") Integer previousColumnId, @Column("EVENT_LABEL_NAME") String labelName, + @Column("EVENT_LABEL_TYPE") LabelType labelType, @Column("EVENT_VALUE_INT") Integer valueInt, + @Column("EVENT_VALUE_STRING") String valueString, @Column("EVENT_VALUE_TIMESTAMP") Date valueTimestamp, + @Column("EVENT_VALUE_CARD_FK") Integer valueCard, @Column("EVENT_VALUE_USER_FK") Integer valueUser) { + this.id = id; + this.cardId = cardId; + this.userId = userId; + this.event = event; + this.time = time; + + this.dataId = dataId; + this.columnId = columnId; + this.labelName = labelName; + this.labelType = labelType; + + this.previousColumnId = previousColumnId; + this.previousDataId = previousDataId; + this.newDataId = newDataId; + + this.valueInt = valueInt; + this.valueString = valueString; + this.valueTimestamp = valueTimestamp; + this.valueCard = valueCard; + this.valueUser = valueUser; + } + +} diff --git a/src/main/java/io/lavagna/model/EventFull.java b/src/main/java/io/lavagna/model/EventFull.java new file mode 100644 index 000000000..abd57ff6a --- /dev/null +++ b/src/main/java/io/lavagna/model/EventFull.java @@ -0,0 +1,72 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.tuple.ImmutablePair; + +@Getter +public class EventFull { + + private final Event event; + + private final String userProvider; + private final String username; + private final String boardShortName; + private final Integer cardSequenceNumber; + private final String content; + + private final String labelBoardShortName; + private final Integer labelCardSequenceNumber; + + private final String labelUserProvider; + private final String labelUsername; + + public EventFull(Event event, User user, ImmutablePair bc, String content, + ImmutablePair labelCard, User labelUser) { + this.event = event; + this.userProvider = user.getProvider(); + this.username = user.getUsername(); + this.boardShortName = bc.getLeft().getShortName(); + this.cardSequenceNumber = bc.getRight().getSequence(); + this.content = content; + // + + if (labelCard != null) { + this.labelBoardShortName = labelCard.getLeft().getShortName(); + this.labelCardSequenceNumber = labelCard.getRight().getSequence(); + } else { + this.labelBoardShortName = null; + this.labelCardSequenceNumber = null; + } + + if (labelUser != null) { + this.labelUserProvider = labelUser.getProvider(); + this.labelUsername = labelUser.getUsername(); + } else { + this.labelUserProvider = null; + this.labelUsername = null; + } + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/src/main/java/io/lavagna/model/EventsCount.java b/src/main/java/io/lavagna/model/EventsCount.java new file mode 100644 index 000000000..ccacb8dcb --- /dev/null +++ b/src/main/java/io/lavagna/model/EventsCount.java @@ -0,0 +1,38 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import java.util.Calendar; +import java.util.Date; + +import lombok.Getter; + +import org.apache.commons.lang3.time.DateUtils; + +@Getter +public class EventsCount { + + private final long date; + private final long count; + + public EventsCount(@Column("EVENT_DATE") Date date, @Column("EVENT_COUNT") long count) { + this.date = DateUtils.truncate(date, Calendar.DATE).getTime(); + this.count = count; + } +} diff --git a/src/main/java/io/lavagna/model/FileDataLight.java b/src/main/java/io/lavagna/model/FileDataLight.java new file mode 100644 index 000000000..21c629891 --- /dev/null +++ b/src/main/java/io/lavagna/model/FileDataLight.java @@ -0,0 +1,52 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import java.util.Date; + +import lombok.Getter; + +@Getter +public class FileDataLight { + private final Integer referenceId; + private final int cardId; + private final int cardDataId; + private final String digest; + private final int userId; + private final Date time; + private final int size; + private final String name; + private final String contentType; + + public FileDataLight(@Column("CARD_DATA_CARD_ID_FK") int cardId, @Column("CARD_DATA_ID") int cardDataId, + @Column("CARD_DATA_REFERENCE_ID") Integer referenceId, @Column("CARD_DATA_CONTENT") String digest, + @Column("SIZE") int size, @Column("DISPLAYED_NAME") String name, + @Column("CONTENT_TYPE") String contentType, @Column("EVENT_USER_ID_FK") int userId, + @Column("EVENT_TIME") Date time) { + this.cardId = cardId; + this.cardDataId = cardDataId; + this.referenceId = referenceId; + this.digest = digest; + this.size = size; + this.name = name; + this.contentType = contentType; + this.userId = userId; + this.time = time; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/ImportContext.java b/src/main/java/io/lavagna/model/ImportContext.java new file mode 100644 index 000000000..e20a6ffc4 --- /dev/null +++ b/src/main/java/io/lavagna/model/ImportContext.java @@ -0,0 +1,39 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import lombok.Getter; + +@Getter +public class ImportContext { + // map the ids from the imported file to the ids generated by the system + private final Map columns = new HashMap<>(); + private final Map commentsId = new HashMap<>(); + private final Map actionListId = new HashMap<>(); + private final Map actionItemId = new HashMap<>(); + private final Map fileId = new HashMap<>(); + // + + private final Set importedProject = new HashSet<>(); + private final Set importedBoard = new HashSet<>(); + +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/Key.java b/src/main/java/io/lavagna/model/Key.java new file mode 100644 index 000000000..a029aaaa5 --- /dev/null +++ b/src/main/java/io/lavagna/model/Key.java @@ -0,0 +1,46 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +public enum Key { + SETUP_COMPLETE, // + BASE_APPLICATION_URL, // + AUTHENTICATION_METHOD, // + // + LDAP_SERVER_URL, // ldap://localhost:10389 + LDAP_MANAGER_DN, // uid=admin,ou=system + LDAP_MANAGER_PASSWORD, // secret + // + LDAP_USER_SEARCH_BASE, // ou=system + LDAP_USER_SEARCH_FILTER, // uid={0} + // + PERSONA_AUDIENCE, // http://localhost:8080 + // + OAUTH_CONFIGURATION, + // + USE_HTTPS, + // + ENABLE_ANON_USER, + // + SMTP_ENABLED, SMTP_CONFIG, + // + TRELLO_API_KEY, + // + MAX_UPLOAD_FILE_SIZE, // for uploaded content by the user (import data is not under this limit) + // + TEST_PLACEHOLDER +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/Label.java b/src/main/java/io/lavagna/model/Label.java new file mode 100644 index 000000000..6dd0e0066 --- /dev/null +++ b/src/main/java/io/lavagna/model/Label.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.model.CardLabel.LabelType; + +@Getter +public class Label { + + private final String name; + private final boolean unique; + private final LabelType type; + private final int color; + + public Label(String name, boolean unique, LabelType type, int color) { + this.name = name; + this.unique = unique; + this.type = type; + this.color = color; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/LabelAndValue.java b/src/main/java/io/lavagna/model/LabelAndValue.java new file mode 100644 index 000000000..425bb715e --- /dev/null +++ b/src/main/java/io/lavagna/model/LabelAndValue.java @@ -0,0 +1,102 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.CardLabelValue.LabelValue; + +import java.util.Date; + +import lombok.Getter; + +import org.apache.commons.lang3.Validate; + +@Getter +public class LabelAndValue { + + private final int labelId; + private final int labelProjectId; + private final String labelName; + private final int labelColor; + private final LabelType labelType; + private final LabelDomain labelDomain; + private final int labelValueId; + private final int labelValueCardId; + private final int labelValueLabelId; + private final Boolean labelValueUseUniqueIndex; + private final LabelType labelValueType; + private final String labelValueString; + private final Date labelValueTimestamp; + private final Integer labelValueInt; + private final Integer labelValueCard; + private final Integer labelValueUser; + private final Integer labelValueList; + private final LabelValue value; + private final boolean labelUnique; + + public LabelAndValue(@Column("CARD_LABEL_ID") int labelId, @Column("CARD_LABEL_PROJECT_ID_FK") int labelProjectId, + @Column("CARD_LABEL_UNIQUE") boolean labelUnique, @Column("CARD_LABEL_TYPE") LabelType labelType, + @Column("CARD_LABEL_DOMAIN") LabelDomain labelDomain, @Column("CARD_LABEL_NAME") String labelName, + @Column("CARD_LABEL_COLOR") int labelColor, @Column("CARD_LABEL_VALUE_ID") int labelValueId, + @Column("CARD_ID_FK") int labelValueCardId, @Column("CARD_LABEL_ID_FK") int labelValueLabelId, + @Column("CARD_LABEL_VALUE_USE_UNIQUE_INDEX") Boolean labelValueUseUniqueIndex, + @Column("CARD_LABEL_VALUE_TYPE") LabelType labelValueType, + @Column("CARD_LABEL_VALUE_STRING") String labelValueString, + @Column("CARD_LABEL_VALUE_TIMESTAMP") Date labelValueTimestamp, + @Column("CARD_LABEL_VALUE_INT") Integer labelValueInt, + @Column("CARD_LABEL_VALUE_CARD_FK") Integer labelValueCard, + @Column("CARD_LABEL_VALUE_USER_FK") Integer labelValueUser, + @Column("CARD_LABEL_VALUE_LIST_VALUE_FK") Integer labelValueList) { + + Validate.isTrue(labelType == labelValueType, "label type is not equal to label value type"); + + this.labelId = labelId; + this.labelProjectId = labelProjectId; + this.labelUnique = labelUnique; + this.labelType = labelType; + this.labelDomain = labelDomain; + this.labelName = labelName; + this.labelColor = labelColor; + // / + this.labelValueId = labelValueId; + this.labelValueCardId = labelValueCardId; + this.labelValueLabelId = labelValueLabelId; + this.labelValueUseUniqueIndex = labelValueUseUniqueIndex; + this.labelValueType = labelValueType; + this.labelValueString = labelValueString; + this.labelValueTimestamp = labelValueTimestamp; + this.labelValueInt = labelValueInt; + this.labelValueCard = labelValueCard; + this.labelValueUser = labelValueUser; + this.labelValueList = labelValueList; + + this.value = new LabelValue(labelValueString, labelValueTimestamp, labelValueInt, labelValueCard, + labelValueUser, labelValueList); + } + + public CardLabel label() { + return new CardLabel(labelId, labelProjectId, labelUnique, labelType, labelDomain, labelName, labelColor); + } + + public CardLabelValue labelValue() { + return new CardLabelValue(labelValueId, labelValueCardId, labelValueLabelId, labelValueUseUniqueIndex, + labelValueType, labelValueString, labelValueTimestamp, labelValueInt, labelValueCard, labelValueUser, + labelValueList); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/LabelAndValueWithCount.java b/src/main/java/io/lavagna/model/LabelAndValueWithCount.java new file mode 100644 index 000000000..fa3939e97 --- /dev/null +++ b/src/main/java/io/lavagna/model/LabelAndValueWithCount.java @@ -0,0 +1,47 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.CardLabel.LabelType; +import lombok.Getter; + +@Getter +public final class LabelAndValueWithCount { + + private final int labelId; + private final String labelName; + private final int labelColor; + private final LabelType labelValueType; + private final CardLabelValue.LabelValue value; + private final Long count; + + public LabelAndValueWithCount(@Column("CARD_LABEL_ID") int labelId, @Column("CARD_LABEL_NAME") String labelName, + @Column("CARD_LABEL_COLOR") int labelColor, @Column("CARD_LABEL_VALUE_TYPE") LabelType labelValueType, + @Column("CARD_LABEL_VALUE_LIST_VALUE_FK") Integer labelValueList, @Column("LABEL_COUNT") Long count) { + + this.labelId = labelId; + this.labelName = labelName; + this.labelColor = labelColor; + if (labelValueType != LabelType.NULL && labelValueType != LabelType.LIST) { + labelValueType = LabelType.NULL; + } + this.labelValueType = labelValueType; + this.count = count; + this.value = new CardLabelValue.LabelValue(null, null, null, null, null, labelValueList); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/LabelListValue.java b/src/main/java/io/lavagna/model/LabelListValue.java new file mode 100644 index 000000000..0380b7a0f --- /dev/null +++ b/src/main/java/io/lavagna/model/LabelListValue.java @@ -0,0 +1,59 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +public class LabelListValue { + + private final int id; + private final int cardLabelId; + private final int order; + private final String value; + + public LabelListValue(@Column("CARD_LABEL_LIST_VALUE_ID") int id, @Column("CARD_LABEL_ID_FK") int cardLabelId, + @Column("CARD_LABEL_LIST_VALUE_ORDER") int order, @Column("CARD_LABEL_LIST_VALUE") String value) { + this.id = id; + this.cardLabelId = cardLabelId; + this.order = order; + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof LabelListValue)) { + return false; + } + LabelListValue other = (LabelListValue) obj; + return new EqualsBuilder().append(id, other.id).append(cardLabelId, other.cardLabelId) + .append(order, other.order).append(value, other.value).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).append(cardLabelId).append(order).append(value).toHashCode(); + } + + public LabelListValue newValue(String newValue) { + return new LabelListValue(id, cardLabelId, order, newValue); + } +} diff --git a/src/main/java/io/lavagna/model/MailConfig.java b/src/main/java/io/lavagna/model/MailConfig.java new file mode 100644 index 000000000..5e8e3218e --- /dev/null +++ b/src/main/java/io/lavagna/model/MailConfig.java @@ -0,0 +1,111 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.internet.MimeMessage; + +import lombok.Getter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.mail.javamail.MimeMessagePreparator; + +/** + * See https://javamail.java.net/nonav/docs/api/com/sun/mail/smtp/package-summary.html for additional parameters. + */ +@Getter +public class MailConfig { + + private static final Logger LOG = LogManager.getLogger(); + + private final String host; + private final Integer port; + private final String protocol; + private final String username; + private final String password; + private final String from; + private final String properties; + + public MailConfig(String host, int port, String protocol, String username, String password, String from, + String properties) { + this.host = host; + this.port = port; + this.protocol = protocol; + this.username = username; + this.password = password; + this.from = from; + this.properties = properties; + } + + public boolean isMinimalConfigurationPresent() { + return StringUtils.isNotBlank(host) && port != null && StringUtils.isNotBlank(protocol) + && StringUtils.isNotBlank(from); + } + + public void send(final String to, final String subject, final String text) { + send(to, subject, text, null); + } + + public void send(final String to, final String subject, final String text, final String html) { + + toMailSender().send(new MimeMessagePreparator() { + @Override + public void prepare(MimeMessage mimeMessage) throws Exception { + MimeMessageHelper message = html == null ? new MimeMessageHelper(mimeMessage, "UTF-8") + : new MimeMessageHelper(mimeMessage, true, "UTF-8"); + message.setSubject(subject); + message.setFrom(getFrom()); + message.setTo(to); + if (html == null) { + message.setText(text, false); + } else { + message.setText(text, html); + } + } + }); + } + + private JavaMailSender toMailSender() { + JavaMailSenderImpl r = new JavaMailSenderImpl(); + r.setDefaultEncoding("UTF-8"); + r.setHost(host); + r.setPort(port); + r.setProtocol(protocol); + r.setUsername(username); + r.setPassword(password); + if (properties != null) { + try { + Properties prop = PropertiesLoaderUtils.loadProperties(new EncodedResource(new ByteArrayResource( + properties.getBytes("UTF-8")), "UTF-8")); + r.setJavaMailProperties(prop); + } catch (IOException e) { + LOG.warn("error while setting the mail sender properties", e); + } + } + return r; + } +} diff --git a/src/main/java/io/lavagna/model/MilestoneCount.java b/src/main/java/io/lavagna/model/MilestoneCount.java new file mode 100644 index 000000000..65fa3191a --- /dev/null +++ b/src/main/java/io/lavagna/model/MilestoneCount.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper; + +@Getter +public class MilestoneCount { + + private final Integer milestoneId; + private final ColumnDefinition columnDefinition; + private final long count; + + public MilestoneCount(@ConstructorAnnotationRowMapper.Column("CARD_LABEL_VALUE_LIST_VALUE_FK") Integer milestoneId, + @ConstructorAnnotationRowMapper.Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition columnDefinition, + @ConstructorAnnotationRowMapper.Column("MILESTONE_COUNT") long count) { + this.milestoneId = milestoneId; + this.columnDefinition = columnDefinition; + this.count = count; + } +} diff --git a/src/main/java/io/lavagna/model/Pair.java b/src/main/java/io/lavagna/model/Pair.java new file mode 100644 index 000000000..976b2246a --- /dev/null +++ b/src/main/java/io/lavagna/model/Pair.java @@ -0,0 +1,35 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; + +@Getter +public class Pair { + + private final T first; + private final U second; + + public Pair(T first, U second) { + this.first = first; + this.second = second; + } + + public static Pair of(T first, U second) { + return new Pair<>(first, second); + } +} diff --git a/src/main/java/io/lavagna/model/Permission.java b/src/main/java/io/lavagna/model/Permission.java new file mode 100644 index 000000000..8c1cf84d1 --- /dev/null +++ b/src/main/java/io/lavagna/model/Permission.java @@ -0,0 +1,135 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.commons.lang3.Validate; + +public enum Permission { + /* access to the admin tool */ + ADMINISTRATION(PermissionCategory.APPLICATION, true), // + + /* role for admin project */ + PROJECT_ADMINISTRATION(PermissionCategory.PROJECT), + + /* role for reading a project */ + CREATE_PROJECT(PermissionCategory.PROJECT, true), + + /* role for reading a board and related column/card/comments */ + READ(PermissionCategory.BOARD), + + /* can create new board */ + CREATE_BOARD(PermissionCategory.BOARD), // + + UPDATE_BOARD(PermissionCategory.BOARD), // + DELETE_BOARD(PermissionCategory.BOARD), // + + /* can create new column */ + CREATE_COLUMN(PermissionCategory.COLUMN), // + + /* can reorder column */ + MOVE_COLUMN(PermissionCategory.COLUMN), // + + /* can rename column */ + RENAME_COLUMN(PermissionCategory.COLUMN), // + + /* can create cards */ + CREATE_CARD(PermissionCategory.CARD), // + UPDATE_CARD(PermissionCategory.CARD), // + MOVE_CARD(PermissionCategory.CARD), // + + // + /* can create a comment */ + CREATE_CARD_COMMENT(PermissionCategory.CARD), // + UPDATE_CARD_COMMENT(PermissionCategory.CARD), // + DELETE_CARD_COMMENT(PermissionCategory.CARD), // + // + CREATE_ACTION_LIST(PermissionCategory.CARD), // + DELETE_ACTION_LIST(PermissionCategory.CARD), // + UPDATE_ACTION_LIST(PermissionCategory.CARD), // + ORDER_ACTION_LIST(PermissionCategory.CARD), // + // + CREATE_ACTION_LIST_ITEM(PermissionCategory.CARD), // + DELETE_ACTION_LIST_ITEM(PermissionCategory.CARD), // + TOGGLE_ACTION_LIST_ITEM(PermissionCategory.CARD), // + UPDATE_ACTION_LIST_ITEM(PermissionCategory.CARD), // + MOVE_ACTION_LIST_ITEM(PermissionCategory.CARD), // + // + + // file related + CREATE_FILE(PermissionCategory.CARD), // + UPDATE_FILE(PermissionCategory.CARD), // + DELETE_FILE(PermissionCategory.CARD), // + // + + // label related + CREATE_LABEL(PermissionCategory.CARD), // + UPDATE_LABEL(PermissionCategory.CARD), // + DELETE_LABEL(PermissionCategory.CARD), // + + MANAGE_LABEL_VALUE(PermissionCategory.CARD), // + // + + /* can update the _current_ user profile */ + UPDATE_PROFILE(PermissionCategory.APPLICATION), // + + SEARCH(PermissionCategory.APPLICATION); + + private final PermissionCategory category; + + /** + * only for base permission, if false: cannot be used in Project level permission + */ + private final boolean onlyForBase; + + private static final Set AVAILABLE_PERMISSION_FOR_PROJECT; + + static { + Set p = EnumSet.noneOf(Permission.class); + for (Permission perm : Permission.values()) { + if (!perm.onlyForBase) { + p.add(perm); + } + } + AVAILABLE_PERMISSION_FOR_PROJECT = Collections.unmodifiableSet(p); + } + + public static void ensurePermissionForProject(Set permissions) { + Validate.isTrue(Permission.AVAILABLE_PERMISSION_FOR_PROJECT.containsAll(permissions), + "permission at project level only: " + permissions + " contain a onlyForBase Permission."); + } + + private Permission(PermissionCategory category, boolean onlyForBase) { + this.category = category; + this.onlyForBase = onlyForBase; + } + + private Permission(PermissionCategory category) { + this(category, false); + } + + public PermissionCategory getCategory() { + return category; + } + + public boolean isOnlyForBase() { + return onlyForBase; + } +} diff --git a/src/main/java/io/lavagna/model/PermissionCategory.java b/src/main/java/io/lavagna/model/PermissionCategory.java new file mode 100644 index 000000000..720097dc6 --- /dev/null +++ b/src/main/java/io/lavagna/model/PermissionCategory.java @@ -0,0 +1,38 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +public enum PermissionCategory { + APPLICATION(true), PROJECT, BOARD, COLUMN, CARD; + + /** + * only for base permission, if false: cannot be used in Project level permission + */ + private final boolean onlyForBase; + + private PermissionCategory() { + onlyForBase = false; + } + + private PermissionCategory(boolean onlyForBase) { + this.onlyForBase = onlyForBase; + } + + public boolean isOnlyForBase() { + return onlyForBase; + } +} diff --git a/src/main/java/io/lavagna/model/Project.java b/src/main/java/io/lavagna/model/Project.java new file mode 100644 index 000000000..a78f5ea7d --- /dev/null +++ b/src/main/java/io/lavagna/model/Project.java @@ -0,0 +1,40 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class Project { + + private final int id; + private final String name; + private final String shortName; + private final String description; + private final boolean archived; + + public Project(@Column("PROJECT_ID") int id, @Column("PROJECT_NAME") String name, + @Column("PROJECT_SHORT_NAME") String shortName, @Column("PROJECT_DESCRIPTION") String description, + @Column("PROJECT_ARCHIVED") boolean archived) { + this.id = id; + this.name = name; + this.shortName = shortName; + this.description = description; + this.archived = archived; + } +} diff --git a/src/main/java/io/lavagna/model/ProjectAndBoard.java b/src/main/java/io/lavagna/model/ProjectAndBoard.java new file mode 100644 index 000000000..c3dd2dbbd --- /dev/null +++ b/src/main/java/io/lavagna/model/ProjectAndBoard.java @@ -0,0 +1,40 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class ProjectAndBoard { + + private final Project project; + private final Board board; + + public ProjectAndBoard( + @Column("PROJECT_ID") int projectId, + @Column("PROJECT_NAME") String projectName, + @Column("PROJECT_SHORT_NAME") String projectShortName, + @Column("PROJECT_DESCRIPTION") String projectDescription, + @Column("PROJECT_ARCHIVED") boolean projectArchived,// + @Column("BOARD_ID") int boardId, @Column("BOARD_NAME") String boardName, + @Column("BOARD_SHORT_NAME") String boardShortName, @Column("BOARD_DESCRIPTION") String boardDescription, + @Column("BOARD_ARCHIVED") boolean boardArchived) { + this.project = new Project(projectId, projectName, projectShortName, projectDescription, projectArchived); + this.board = new Board(boardId, boardName, boardShortName, boardDescription, projectId, boardArchived); + } +} diff --git a/src/main/java/io/lavagna/model/ProjectRoleAndPermission.java b/src/main/java/io/lavagna/model/ProjectRoleAndPermission.java new file mode 100644 index 000000000..25d95b003 --- /dev/null +++ b/src/main/java/io/lavagna/model/ProjectRoleAndPermission.java @@ -0,0 +1,46 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class ProjectRoleAndPermission { + + final String projectShortName; + final int projectId; + private final String roleName; + private final boolean removable; + + /** can be null */ + final Permission permission; + + /** is null if permission is null */ + private final PermissionCategory category; + + public ProjectRoleAndPermission(@Column("PROJECT_ID") int projectId, + @Column("PROJECT_SHORT_NAME") String projectShortName, @Column("ROLE_NAME") String roleName, + @Column("ROLE_REMOVABLE") boolean removable, @Column("PERMISSION") Permission permission) { + this.projectShortName = projectShortName; + this.projectId = projectId; + this.roleName = roleName; + this.removable = removable; + this.permission = permission; + this.category = permission != null ? permission.getCategory() : null; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/ProjectWithEventCounts.java b/src/main/java/io/lavagna/model/ProjectWithEventCounts.java new file mode 100644 index 000000000..8b0fd5563 --- /dev/null +++ b/src/main/java/io/lavagna/model/ProjectWithEventCounts.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class ProjectWithEventCounts { + + private final long events; + private final Project project; + + public ProjectWithEventCounts(@Column("PROJECT_ID") int projectId, @Column("PROJECT_NAME") String projectName, + @Column("PROJECT_SHORT_NAME") String projectShortName, + @Column("PROJECT_DESCRIPTION") String projectDescription, + @Column("PROJECT_ARCHIVED") boolean projectArchived,// + @Column("EVENTS") long events) { + this.project = new Project(projectId, projectName, projectShortName, projectDescription, projectArchived); + this.events = events; + } +} diff --git a/src/main/java/io/lavagna/model/Role.java b/src/main/java/io/lavagna/model/Role.java new file mode 100644 index 000000000..7c7953ebf --- /dev/null +++ b/src/main/java/io/lavagna/model/Role.java @@ -0,0 +1,45 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.Locale; +import java.util.Objects; + +import lombok.Getter; + +@Getter +public final class Role { + + private final String name; + + public Role(String name) { + this.name = name.toUpperCase(Locale.ENGLISH); + } + + @Override + public boolean equals(Object obj) { + return (obj != null && obj instanceof Role) ? Objects.equals(name, ((Role) obj).name) : false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + public static final Role ADMIN_ROLE = new Role("ADMIN"); + public static final Role DEFAULT_ROLE = new Role("DEFAULT"); +} diff --git a/src/main/java/io/lavagna/model/RoleAndMetadata.java b/src/main/java/io/lavagna/model/RoleAndMetadata.java new file mode 100644 index 000000000..c616c3f56 --- /dev/null +++ b/src/main/java/io/lavagna/model/RoleAndMetadata.java @@ -0,0 +1,40 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class RoleAndMetadata { + + private final String roleName; + + private final boolean removable; + + private final boolean hidden; + + private final boolean readOnly; + + public RoleAndMetadata(@Column("ROLE_NAME") String roleName, @Column("ROLE_REMOVABLE") boolean removable, + @Column("ROLE_HIDDEN") boolean hidden, @Column("ROLE_READONLY") boolean readOnly) { + this.roleName = roleName; + this.removable = removable; + this.hidden = hidden; + this.readOnly = readOnly; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/RoleAndPermission.java b/src/main/java/io/lavagna/model/RoleAndPermission.java new file mode 100644 index 000000000..d5b9301f5 --- /dev/null +++ b/src/main/java/io/lavagna/model/RoleAndPermission.java @@ -0,0 +1,61 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.Objects; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +@Getter +public class RoleAndPermission { + private final String roleName; + + private final boolean removable; + + private final boolean hidden; + + private final boolean readOnly; + + /** can be null */ + private final Permission permission; + + /** if permission is null, category is null too */ + private final PermissionCategory category; + + public RoleAndPermission(@Column("ROLE_NAME") String roleName, @Column("ROLE_REMOVABLE") boolean removable, + @Column("ROLE_HIDDEN") boolean hidden, @Column("ROLE_READONLY") boolean readOnly, + @Column("PERMISSION") Permission permission) { + this.roleName = roleName; + this.removable = removable; + this.hidden = hidden; + this.readOnly = readOnly; + this.permission = permission; + this.category = permission != null ? permission.getCategory() : null; + } + + @Override + public boolean equals(Object obj) { + return (obj != null && obj instanceof RoleAndPermission) ? Objects.equals(roleName, + ((RoleAndPermission) obj).roleName) : false; + } + + @Override + public int hashCode() { + return roleName.hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/SearchResults.java b/src/main/java/io/lavagna/model/SearchResults.java new file mode 100644 index 000000000..a0179744f --- /dev/null +++ b/src/main/java/io/lavagna/model/SearchResults.java @@ -0,0 +1,39 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class SearchResults { + + private final List found; + private final int count; + private final int currentPage; + private final int countPerPage; + private final int totalPages; + + public SearchResults(List found, int count, int currentPage, int countPerPage) { + this.found = found; + this.count = count; + this.currentPage = currentPage; + this.countPerPage = countPerPage; + totalPages = (count + countPerPage - 1) / countPerPage; + } +} diff --git a/src/main/java/io/lavagna/model/StatisticForExport.java b/src/main/java/io/lavagna/model/StatisticForExport.java new file mode 100644 index 000000000..de19d9deb --- /dev/null +++ b/src/main/java/io/lavagna/model/StatisticForExport.java @@ -0,0 +1,43 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; +import io.lavagna.model.BoardColumn.BoardColumnLocation; + +import java.util.Date; + +import lombok.Getter; + +@Getter +public class StatisticForExport { + + private final Date date; + private final ColumnDefinition columnDefinition; + private final BoardColumnLocation location; + private final long count; + + public StatisticForExport(@Column("BOARD_STATISTICS_TIME") Date date, + @Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition columnDefinition, + @Column("BOARD_STATISTICS_LOCATION") BoardColumnLocation location, + @Column("BOARD_STATISTICS_COUNT") long count) { + this.date = date; + this.columnDefinition = columnDefinition; + this.location = location; + this.count = count; + } +} diff --git a/src/main/java/io/lavagna/model/StatisticsResult.java b/src/main/java/io/lavagna/model/StatisticsResult.java new file mode 100644 index 000000000..0ec379e00 --- /dev/null +++ b/src/main/java/io/lavagna/model/StatisticsResult.java @@ -0,0 +1,42 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import java.util.Calendar; +import java.util.Date; + +import lombok.Getter; + +import org.apache.commons.lang3.time.DateUtils; + +@Getter +public class StatisticsResult { + + private final long day; + private final ColumnDefinition columnDefinition; + private final long count; + + public StatisticsResult(@Column("TIME") Date date, + @Column("BOARD_COLUMN_DEFINITION_VALUE") ColumnDefinition columnDefinition, + @Column("STATISTICS_COUNT") long count) { + this.day = DateUtils.truncate(date, Calendar.DATE).getTime(); + this.columnDefinition = columnDefinition; + this.count = count; + } +} diff --git a/src/main/java/io/lavagna/model/User.java b/src/main/java/io/lavagna/model/User.java new file mode 100644 index 000000000..c1660fbe9 --- /dev/null +++ b/src/main/java/io/lavagna/model/User.java @@ -0,0 +1,79 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +import java.util.Date; + +import lombok.Getter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +public class User { + + private final int id; + private final String provider; + private final String username; + private final String email; + private final String displayName; + private final boolean enabled; + private final boolean emailNotification; + private final Date memberSince; + + public User(@Column("USER_ID") int id, @Column("USER_PROVIDER") String provider, + @Column("USER_NAME") String username, @Column("USER_EMAIL") String email, + @Column("USER_DISPLAY_NAME") String displayName, @Column("USER_ENABLED") Boolean enabled, + @Column("USER_EMAIL_NOTIFICATION") boolean emailNotification, @Column("USER_MEMBER_SINCE") Date memberSince) { + this.id = id; + this.username = username; + this.provider = provider; + this.email = email; + this.displayName = displayName; + this.enabled = enabled == null ? true : enabled; + this.emailNotification = emailNotification; + this.memberSince = memberSince; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof User)) { + return false; + } + User u = (User) obj; + return new EqualsBuilder().append(id, u.id).append(provider, u.provider).append(username, u.username) + .append(email, u.email).append(displayName, u.displayName).append(enabled, u.enabled) + .append(emailNotification, u.emailNotification).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).append(provider).append(username).append(email).append(displayName) + .append(enabled).append(emailNotification).toHashCode(); + } + + public boolean isAnonymous() { + return "system".equals(provider) && "anonymous".equals(username); + } + + public boolean canSendEmail() { + return enabled && emailNotification && StringUtils.isNotBlank(email); + } +} diff --git a/src/main/java/io/lavagna/model/UserIdentifier.java b/src/main/java/io/lavagna/model/UserIdentifier.java new file mode 100644 index 000000000..5d1f7762a --- /dev/null +++ b/src/main/java/io/lavagna/model/UserIdentifier.java @@ -0,0 +1,35 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import lombok.Getter; +import io.lavagna.common.ConstructorAnnotationRowMapper.Column; + +/** + * Identify a user in the Lavagna system + */ +@Getter +public class UserIdentifier { + + private final String provider; + private final String username; + + public UserIdentifier(@Column("USER_PROVIDER") String provider, @Column("USER_NAME") String username) { + this.provider = provider; + this.username = username; + } +} diff --git a/src/main/java/io/lavagna/model/UserToCreate.java b/src/main/java/io/lavagna/model/UserToCreate.java new file mode 100644 index 000000000..02c263ade --- /dev/null +++ b/src/main/java/io/lavagna/model/UserToCreate.java @@ -0,0 +1,43 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserToCreate { + + private String provider; + private String username; + private String email; + private String displayName; + private boolean enabled; + private List roles; + + public UserToCreate() { + } + + public UserToCreate(String provider, String username) { + this.provider = provider; + this.username = username; + enabled = true; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/UserWithPermission.java b/src/main/java/io/lavagna/model/UserWithPermission.java new file mode 100644 index 000000000..48417c235 --- /dev/null +++ b/src/main/java/io/lavagna/model/UserWithPermission.java @@ -0,0 +1,97 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import lombok.Getter; + +import org.apache.commons.lang3.Validate; + +@Getter +public class UserWithPermission extends User { + + private final Map basePermissions; + private final Map> permissionsForProject; + private final Map> permissionsForProjectId; + + public UserWithPermission(User user, Set permissions, + Map> permissionsForProject, Map> permissionsForProjectId) { + super(user.getId(), user.getProvider(), user.getUsername(), user.getEmail(), user.getDisplayName(), user + .isEnabled(), user.isEmailNotification(), user.getMemberSince()); + // identity map + this.basePermissions = identityMap(permissions); + + this.permissionsForProject = new HashMap<>(); + for (Entry> pp : permissionsForProject.entrySet()) { + this.permissionsForProject.put(pp.getKey(), identityMap(pp.getValue())); + } + + this.permissionsForProjectId = new HashMap<>(); + for (Entry> pp : permissionsForProjectId.entrySet()) { + this.permissionsForProjectId.put(pp.getKey(), identityMap(pp.getValue())); + } + } + + public Set projectsWithPermission(Permission p) { + Set s = new HashSet<>(); + for (Entry> f : permissionsForProject.entrySet()) { + if (f.getValue().containsKey(p)) { + s.add(f.getKey()); + } + } + return s; + } + + public Set projectsIdWithPermission(Permission p) { + Set s = new HashSet<>(); + for (Entry> f : permissionsForProjectId.entrySet()) { + if (f.getValue().containsKey(p)) { + s.add(f.getKey()); + } + } + return s; + } + + private static Map identityMap(Set permissions) { + Map res = new EnumMap<>(Permission.class); + for (Permission p : permissions) { + res.put(p, p); + } + return res; + } + + public Set toProjectIdsFilter(Integer projectId) { + Set projectIdFilter = new HashSet<>(); + boolean hasGlobalRead = getBasePermissions().containsKey(Permission.READ); + Set projectIdsWithReadPermission = projectsIdWithPermission(Permission.READ); + if (projectId != null) { + if (!hasGlobalRead) { + Validate.isTrue(projectIdsWithReadPermission.contains(projectId)); + } + projectIdFilter.add(projectId); + } else if (projectId == null && !hasGlobalRead) { + projectIdFilter.addAll(projectIdsWithReadPermission); + } + return projectIdFilter; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/model/util/ShortNameGenerator.java b/src/main/java/io/lavagna/model/util/ShortNameGenerator.java new file mode 100644 index 000000000..dcf7e047c --- /dev/null +++ b/src/main/java/io/lavagna/model/util/ShortNameGenerator.java @@ -0,0 +1,100 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.model.util; + +import java.util.Locale; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +public final class ShortNameGenerator { + + private ShortNameGenerator() { + } + + public static boolean isShortNameValid(String shortName) { + Pattern acceptedChars = Pattern.compile("^[A-Z0-9_]+$"); + return acceptedChars.matcher(shortName).matches(); + } + + /** + *

+ * Generate a short name given the full project name. + * + * Total length returned is equal or less than 8. + *

+ * + *

+ * Heuristic: + *

+ *
    + *
  • if name is less or equals than 6 chars and is a single word, the short name will be UPPER(name)
  • + *
  • if the project has multiple words, it will concatenate each words. If a word has a length more than 4: + *
      + *
    • it will take only the upper case characters if there are more than 1.
    • + *
    • else it will take the first two characters.
    • + *
    + *
  • + *
+ * + * @param name + * @return + */ + public static String generateShortNameFrom(String name) { + if (StringUtils.isBlank(name)) { + return name; + } + + String t = name.trim().replace("-", ""); + String[] splitted = t.split("\\s+"); + if (splitted.length == 1) { + return splitted[0].substring(0, Math.min(6, splitted[0].length())).toUpperCase(Locale.ENGLISH); + } + StringBuilder sb = new StringBuilder(); + for (String token : splitted) { + if (token.length() <= 4) { + sb.append(token); + } else if (countUpperCase(token) > 1) { + sb.append(takeFirstFourUpperCaseChars(token)); + } else { + sb.append(token.substring(0, 3)); + } + } + return sb.toString().toUpperCase(Locale.ENGLISH).substring(0, Math.min(8, sb.length())); + } + + private static String takeFirstFourUpperCaseChars(String s) { + StringBuilder sb = new StringBuilder(); + for (char c : s.toCharArray()) { + if (Character.isUpperCase(c)) { + sb.append(c); + } + } + return sb.substring(0, Math.min(sb.length(), 4)); + } + + private static int countUpperCase(String s) { + int cnt = 0; + for (char c : s.toCharArray()) { + if (Character.isUpperCase(c)) { + cnt++; + } + } + return cnt; + } + +} diff --git a/src/main/java/io/lavagna/query/BoardColumnQuery.java b/src/main/java/io/lavagna/query/BoardColumnQuery.java new file mode 100644 index 000000000..23f71dbcd --- /dev/null +++ b/src/main/java/io/lavagna/query/BoardColumnQuery.java @@ -0,0 +1,95 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.BoardColumnInfo; + +import java.util.List; +import java.util.Set; + +@QueryRepository +public interface BoardColumnQuery { + + @Query("SELECT * FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_ID = :columnId") + BoardColumn findById(@Bind("columnId") int columnId); + + @Query("SELECT * FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_ID IN (:ids)") + List findByIds(@Bind("ids") Set ids); + + @Query("SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID IN (:ids) AND BOARD_COLUMN_LOCATION = :location AND BOARD_COLUMN_BOARD_ID_FK = :boardId") + List findColumnIdsInBoard(@Bind("ids") List ids, @Bind("location") String location, @Bind("boardId") int boardId); + + @Query("SELECT CARD_ID FROM LA_CARD WHERE CARD_BOARD_COLUMN_ID_FK = :columnId") + List findCardsInColumnId(@Bind("columnId") int columnId); + + @Query("INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES " + + "(:name, (SELECT * FROM (SELECT COALESCE(MAX(BOARD_COLUMN_ORDER),0) + 1 FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_BOARD_ID_FK = :boardId AND BOARD_COLUMN_LOCATION = :location) AS MAX_BOARD_COLUMN_ORDER), " + + ":boardId, :location, :definitionId)") + int addColumnToBoard(@Bind("name") String name, @Bind("boardId") int boardId, @Bind("location") String location, + @Bind("definitionId") int definitionId); + + @Query("SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE," + + " BOARD_COLUMN_DEFINITION_COLOR FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_BOARD_ID_FK = :boardId AND BOARD_COLUMN_LOCATION = :location " + + "ORDER BY BOARD_COLUMN_ORDER ASC, BOARD_COLUMN_NAME ASC") + List findAllColumnFor(@Bind("boardId") int boardId, @Bind("location") String location); + + @Query("SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE," + + " BOARD_COLUMN_DEFINITION_COLOR FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_BOARD_ID_FK = :boardId " + + "ORDER BY BOARD_COLUMN_ORDER ASC, BOARD_COLUMN_NAME ASC") + List findAllColumnFor(@Bind("boardId") int boardId); + + @Query("SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR FROM LA_BOARD_COLUMN_FULL " + + "WHERE BOARD_COLUMN_BOARD_ID_FK = :boardId AND BOARD_COLUMN_LOCATION = :location AND BOARD_COLUMN_NAME = :location") + BoardColumn findDefaultColumnFor(@Bind("boardId") int boardId, @Bind("location") String location); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_BOARD_COLUMN SET BOARD_COLUMN_ORDER = :order WHERE BOARD_COLUMN_ID = :columnId AND BOARD_COLUMN_BOARD_ID_FK = :boardId AND BOARD_COLUMN_LOCATION = :location") + String updateColumnOrder(); + + @Query("UPDATE LA_BOARD_COLUMN SET BOARD_COLUMN_LOCATION = :location, BOARD_COLUMN_DEFINITION_ID_FK = :columnDefinitionId WHERE BOARD_COLUMN_ID = :columnId") + int moveToLocation(@Bind("columnId") int columnId, @Bind("location") String location, + @Bind("columnDefinitionId") int columnDefinitionId); + + @Query("UPDATE LA_BOARD_COLUMN SET BOARD_COLUMN_ORDER = :order WHERE BOARD_COLUMN_ID = :columnId") + int updateOrder(@Bind("columnId") int columnId, @Bind("order") int order); + + @Query("UPDATE LA_BOARD_COLUMN SET BOARD_COLUMN_NAME = :newName WHERE BOARD_COLUMN_ID = :columnId AND BOARD_COLUMN_BOARD_ID_FK = :boardId") + int renameColumn(@Bind("newName") String newName, @Bind("columnId") int columnId, @Bind("boardId") int boardId); + + @Query("SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR " + + " FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR " + + " FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_ID = LAST_INSERT_ID()"),// + @QueryOverride(db = DB.PGSQL, value = "SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR " + + " FROM LA_BOARD_COLUMN_FULL WHERE BOARD_COLUMN_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_board_column','board_column_id')))") }) + BoardColumn findLastCreatedColumn(); + + @Query("SELECT * FROM LA_BOARD_COLUMN_INFO WHERE BOARD_COLUMN_ID = :columnId") + BoardColumnInfo getColumnInfoById(@Bind("columnId") int columnId); + + @Query("UPDATE LA_BOARD_COLUMN SET BOARD_COLUMN_DEFINITION_ID_FK = :definitionId WHERE BOARD_COLUMN_ID = :columnId AND BOARD_COLUMN_BOARD_ID_FK = :boardId") + int redefineColumn(@Bind("definitionId") int definitionId, @Bind("columnId") int columnId, + @Bind("boardId") int boardId); + +} diff --git a/src/main/java/io/lavagna/query/BoardQuery.java b/src/main/java/io/lavagna/query/BoardQuery.java new file mode 100644 index 000000000..0079cddeb --- /dev/null +++ b/src/main/java/io/lavagna/query/BoardQuery.java @@ -0,0 +1,82 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.model.Board; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.BoardInfo; +import io.lavagna.model.ProjectAndBoard; + +import java.util.List; + +@QueryRepository +public interface BoardQuery { + + @Query("INSERT INTO LA_BOARD(BOARD_NAME, BOARD_SHORT_NAME, BOARD_DESCRIPTION, BOARD_PROJECT_ID_FK) VALUES(:name, :shortName, :description, :projectId)") + int createNewBoard(@Bind("name") String name, @Bind("shortName") String shortName, + @Bind("description") String description, @Bind("projectId") int projectId); + + @Query("UPDATE LA_BOARD SET BOARD_NAME = :name, BOARD_DESCRIPTION = :description, BOARD_ARCHIVED = :archived WHERE BOARD_ID = :boardId") + int updateBoard(@Bind("boardId") int boardId, @Bind("name") String name, @Bind("description") String description, + @Bind("archived") boolean archived); + + @Query("SELECT * FROM LA_BOARD WHERE BOARD_SHORT_NAME = :shortName") + Board findBoardByShortName(@Bind("shortName") String shortName); + + @Query("SELECT * FROM LA_BOARD WHERE BOARD_ID = :boardId") + Board findBoardById(@Bind("boardId") int boardId); + + @Query("SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_SHORT_NAME = :shortName") + Integer findBoardIdByShortName(@Bind("shortName") String shortName); + + @Query("SELECT * FROM LA_BOARD WHERE BOARD_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_BOARD WHERE BOARD_ID = LAST_INSERT_ID()"),// + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_BOARD WHERE BOARD_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_board','board_id')))"), }) + Board findLastCreatedBoard(); + + @Query("INSERT INTO LA_BOARD_COUNTER(BOARD_COUNTER_ID_FK, BOARD_COUNTER_CARD_SEQUENCE) VALUES(IDENTITY() , 1)") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "INSERT INTO LA_BOARD_COUNTER(BOARD_COUNTER_ID_FK, BOARD_COUNTER_CARD_SEQUENCE) VALUES (LAST_INSERT_ID(), 1)"),// + @QueryOverride(db = DB.PGSQL, value = "INSERT INTO LA_BOARD_COUNTER(BOARD_COUNTER_ID_FK, BOARD_COUNTER_CARD_SEQUENCE) VALUES ((SELECT CURRVAL(pg_get_serial_sequence('la_board','board_id'))), 1)"), }) + int initializeSequence(); + + @Query("SELECT * FROM LA_BOARD ORDER BY BOARD_SHORT_NAME") + List findAll(); + + @Query("SELECT * FROM LA_BOARD_COLUMN_DEFINITION where BOARD_COLUMN_DEFINITION_VALUE = :definition and BOARD_COLUMN_DEFINITION_PROJECT_ID_FK = :projectId") + BoardColumnDefinition findColumnDefinitionByProjectIdAndType(@Bind("projectId") int projectId, + @Bind("definition") String definition); + + @Query("SELECT BOARD_SHORT_NAME, BOARD_NAME, BOARD_DESCRIPTION, BOARD_ARCHIVED FROM LA_BOARD WHERE BOARD_PROJECT_ID_FK = :projectId ORDER BY BOARD_SHORT_NAME") + List findBoardInfo(@Bind("projectId") int projectId); + + @Query("SELECT LA_PROJECT.PROJECT_ID, LA_PROJECT.PROJECT_NAME, LA_PROJECT.PROJECT_SHORT_NAME, PROJECT_DESCRIPTION, PROJECT_ARCHIVED, " + + "BOARD_ID, LA_BOARD.BOARD_SHORT_NAME, BOARD_NAME, BOARD_DESCRIPTION, BOARD_ARCHIVED FROM LA_PROJECT " + + "INNER JOIN LA_BOARD ON LA_PROJECT.PROJECT_ID = LA_BOARD.BOARD_PROJECT_ID_FK " + + "INNER JOIN LA_BOARD_COLUMN ON LA_BOARD.BOARD_ID = LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK " + + "WHERE LA_BOARD_COLUMN.BOARD_COLUMN_ID = :columnId") + ProjectAndBoard findProjectAndBoardByColumnId(@Bind("columnId") int columnId); + + @Query("SELECT COUNT(BOARD_SHORT_NAME) FROM LA_BOARD WHERE BOARD_SHORT_NAME = :shortName") + Integer existsWithShortName(@Bind("shortName") String shortName); +} diff --git a/src/main/java/io/lavagna/query/CardDataQuery.java b/src/main/java/io/lavagna/query/CardDataQuery.java new file mode 100644 index 000000000..44299bcf5 --- /dev/null +++ b/src/main/java/io/lavagna/query/CardDataQuery.java @@ -0,0 +1,174 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.CardData; +import io.lavagna.model.CardDataCount; +import io.lavagna.model.CardDataFull; +import io.lavagna.model.CardDataIdAndOrder; +import io.lavagna.model.CardDataMetadata; +import io.lavagna.model.CardDataUploadContentInfo; +import io.lavagna.model.CardIdAndContent; +import io.lavagna.model.FileDataLight; + +import java.util.Collection; +import java.util.List; + +@QueryRepository +public interface CardDataQuery { + + @Query("SELECT CARD_DATA_ID, CARD_DATA_CONTENT FROM LA_CARD_DATA WHERE CARD_DATA_ID IN (:ids)") + List findDataByIds(@Bind("ids") Collection ids); + + @Query("SELECT CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_DELETED, CARD_DATA_TYPE, CARD_DATA_ORDER FROM LA_CARD_DATA WHERE CARD_DATA_ID = :id") + CardDataMetadata findMetadataById(@Bind("id") int id); + + @Query("SELECT CARD_DATA_ID, CARD_DATA_CONTENT FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_REFERENCE_ID = :refId AND CARD_DATA_TYPE = :type AND CARD_DATA_ORDER = :order") + List findContentWith(@Bind("cardId") int cardId, @Bind("refId") int refId, + @Bind("type") String type, @Bind("order") int order); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_ID = :id AND CARD_DATA_DELETED = FALSE") + CardData getUndeletedDataLightById(@Bind("id") int id); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_ID = :id") + CardData getDataLightById(@Bind("id") int id); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC, CARD_DATA_ORDER ASC") + @QueriesOverride(@QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC NULLS FIRST, CARD_DATA_ORDER ASC")) + List findAllLightByCardId(@Bind("cardId") int cardId); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId and CARD_DATA_TYPE = :type AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC, CARD_DATA_ORDER ASC") + @QueriesOverride(@QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId and CARD_DATA_TYPE = :type AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC NULLS FIRST, CARD_DATA_ORDER ASC")) + List findAllLightByCardIdAndType(@Bind("cardId") int cardId, @Bind("type") String type); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId and CARD_DATA_TYPE IN (:types) AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC, CARD_DATA_ORDER ASC") + @QueriesOverride(@QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId and CARD_DATA_TYPE IN (:types) AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC NULLS FIRST, CARD_DATA_ORDER ASC")) + List findAllLightByCardIdAndTypes(@Bind("cardId") int cardId, @Bind("types") List types); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_REFERENCE_ID = :referenceId AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_ORDER ASC") + List findAllLightByReferenceId(@Bind("referenceId") int referenceId); + + @Query("SELECT CARD_DATA_ID, CARD_DATA_ORDER FROM LA_CARD_DATA WHERE CARD_DATA_TYPE IN (:types)") + List findAllCardDataIdAndOrderByType(@Bind("types") List types); + + /** + * Will return the deleted one too. + */ + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_REFERENCE_ID = :referenceId AND CARD_DATA_TYPE = :type ORDER BY CARD_DATA_ORDER ASC") + List findAllLightByReferenceIdAndType(@Bind("referenceId") int referenceId, @Bind("type") String type); + + @Query("INSERT INTO LA_CARD_DATA(CARD_DATA_CARD_ID_FK,CARD_DATA_TYPE,CARD_DATA_CONTENT,CARD_DATA_ORDER) " + + " VALUES (:cardId, :type, :content, (SELECT * FROM (SELECT COALESCE(MAX(CARD_DATA_ORDER),0) + 1 FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_TYPE = :type) AS MAX_CARD_DATA_ORDER))") + int create(@Bind("cardId") int cardId, @Bind("type") String type, @Bind("content") String content); + + @Query("INSERT INTO LA_CARD_DATA(CARD_DATA_CARD_ID_FK,CARD_DATA_REFERENCE_ID,CARD_DATA_TYPE,CARD_DATA_CONTENT,CARD_DATA_ORDER) " + + " VALUES (:cardId, :referenceId, :type, :content, (SELECT * FROM (SELECT COALESCE(MAX(CARD_DATA_ORDER),0) + 1 FROM LA_CARD_DATA WHERE CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_REFERENCE_ID = :referenceId) AS MAX_CARD_DATA_ORDER))") + int createWithReferenceOrder(@Bind("cardId") int cardId, @Bind("referenceId") Integer referenceId, + @Bind("type") String type, @Bind("content") String content); + + @Query("SELECT CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_TYPE, CARD_DATA_CONTENT, CARD_DATA_ORDER, EVENT_TIME, EVENT_TYPE, EVENT_PREV_CARD_DATA_ID_FK, EVENT_USER_ID_FK " + + " FROM LA_CARD_DATA_FULL WHERE CARD_DATA_CARD_ID_FK = :cardId and CARD_DATA_TYPE = :type AND CARD_DATA_DELETED = FALSE ORDER BY CARD_DATA_REFERENCE_ID ASC, CARD_DATA_ORDER ASC") + List findAllByCardIdAndType(@Bind("cardId") int cardId, @Bind("type") String type); + + @Query("SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_DATA WHERE CARD_DATA_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_card_data','card_data_id')))") }) + CardData findLastCreatedLight(); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_TYPE = :type WHERE CARD_DATA_ID = :id AND CARD_DATA_TYPE IN (:types)") + int updateType(@Bind("type") String type, @Bind("id") int id, @Bind("types") List types); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_CARD_DATA SET CARD_DATA_ORDER = :order WHERE CARD_DATA_ID = :id AND CARD_DATA_CARD_ID_FK = :cardId") + String updateOrder(); + + @Query("SELECT CARD_DATA_ID FROM LA_CARD_DATA WHERE CARD_DATA_ID IN (:ids) AND CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_TYPE = :cardDataType") + List findAllCardDataIdsBy(@Bind("ids") List ids, @Bind("cardId") int cardId, + @Bind("cardDataType") String cardDataType); + + @Query("SELECT CARD_DATA_ID FROM LA_CARD_DATA WHERE CARD_DATA_ID IN (:ids) AND CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_TYPE IN (:cardDataTypes) AND CARD_DATA_REFERENCE_ID = :referenceId") + List findAllCardDataIdsBy(@Bind("ids") List ids, @Bind("cardId") int cardId, + @Bind("referenceId") int referenceId, @Bind("cardDataTypes") List cardDataTypes); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_ORDER = :order WHERE CARD_DATA_ID = :id") + int updateOrderById(@Bind("id") int id, @Bind("order") int order); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_CARD_DATA SET CARD_DATA_ORDER = :order WHERE CARD_DATA_ID = :id AND CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_REFERENCE_ID = :referenceId") + String updateOrderByCardAndReferenceId(); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_REFERENCE_ID = :referenceId WHERE CARD_DATA_ID = :id AND CARD_DATA_CARD_ID_FK = :cardId") + int updateReferenceId(@Bind("referenceId") Integer referenceId, @Bind("id") int id, @Bind("cardId") int cardId); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_CONTENT = :content WHERE CARD_DATA_ID = :id AND CARD_DATA_TYPE IN (:types)") + int updateContent(@Bind("content") String content, @Bind("id") int id, @Bind("types") List types); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_DELETED = TRUE WHERE CARD_DATA_ID = :id AND CARD_DATA_TYPE IN (:types)") + int softDelete(@Bind("id") int id, @Bind("types") List types); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_DELETED = FALSE WHERE CARD_DATA_ID = :id AND CARD_DATA_TYPE IN (:types)") + int undoSoftDelete(@Bind("id") int id, @Bind("types") List types); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_DELETED = TRUE WHERE (CARD_DATA_ID = :id AND CARD_DATA_TYPE IN (:types)) OR (CARD_DATA_REFERENCE_ID = :id)") + int softDeleteOnCascade(@Bind("id") int id, @Bind("types") List types); + + @Query("UPDATE LA_CARD_DATA SET CARD_DATA_DELETED = FALSE WHERE " + + " ((CARD_DATA_ID = :id AND CARD_DATA_TYPE IN (:types)) OR " + + " (CARD_DATA_REFERENCE_ID = :id)) " + + " AND (CARD_DATA_ID NOT IN (SELECT * FROM (SELECT CARD_DATA_ID FROM LA_CARD_DATA INNER JOIN LA_EVENT ON CARD_DATA_ID = EVENT_CARD_DATA_ID_FK WHERE CARD_DATA_REFERENCE_ID = :id AND EVENT_TYPE IN (:filteredEvents)) AS CDATA_WITH_REF))") + int undoSoftDeleteOnCascade(@Bind("id") int id, @Bind("types") List types, + @Bind("filteredEvents") List filteredEvents); + + @Query("SELECT CARD_ID, CARD_DATA_TYPE, CARD_DATA_TYPE_COUNT FROM LA_CARD_DATA_COUNT" + + " INNER JOIN LA_BOARD_COLUMN ON BOARD_COLUMN_ID = CARD_BOARD_COLUMN_ID_FK" + + " WHERE BOARD_ID = :boardId AND BOARD_COLUMN_LOCATION = :location") + List findCountsByBoardIdAndLocation(@Bind("boardId") int boardId, @Bind("location") String location); + + @Query("SELECT CARD_DATA_CARD_ID_FK AS CARD_ID, CARD_DATA_TYPE, COUNT(CARD_DATA_TYPE) AS CARD_DATA_TYPE_COUNT FROM LA_CARD_DATA WHERE CARD_DATA_DELETED = FALSE AND CARD_DATA_CARD_ID_FK IN (:ids) GROUP BY CARD_DATA_CARD_ID_FK, CARD_DATA_TYPE") + List findCountsByCardIds(@Bind("ids") List ids); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_CARD_DATA_UPLOAD_CONTENT(DIGEST,SIZE,CONTENT,CONTENT_TYPE) VALUES (?, ?, ?, ?)") + String addUploadContent(); + + @Query("SELECT COUNT(1) FROM LA_CARD_DATA_UPLOAD_CONTENT WHERE DIGEST = :digest") + Integer findDigest(@Bind("digest") String digest); + + @Query("SELECT COUNT(1) FROM LA_CARD_DATA_UPLOAD_CONTENT_LIGHT WHERE CARD_DATA_CARD_ID_FK = :cardId AND CARD_DATA_CONTENT = :digest") + Integer isFileAvailableByCard(@Bind("cardId") int cardId, @Bind("digest") String digest); + + @Query("INSERT INTO LA_CARD_DATA_UPLOAD(CARD_DATA_ID_FK,CARD_DATA_UPLOAD_CONTENT_DIGEST_FK,ORIGINAL_NAME,DISPLAYED_NAME) VALUES (:cardData, :digest, :name, :displayName)") + int mapUploadContent(@Bind("cardData") int cardData, @Bind("digest") String digest, @Bind("name") String name, + @Bind("displayName") String displayName); + + @Query("SELECT * FROM LA_CARD_DATA_UPLOAD_CONTENT_LIGHT WHERE CARD_DATA_CARD_ID_FK = :cardId") + List findAllFilesByCardId(@Bind("cardId") int cardId); + + @Query("SELECT * FROM LA_CARD_DATA_UPLOAD_CONTENT_LIGHT WHERE CARD_DATA_ID = :cardDataId") + FileDataLight getUndeletedFileByCardDataId(@Bind("cardDataId") int cardDataId); + + @Query("SELECT DIGEST,SIZE,CONTENT_TYPE FROM LA_CARD_DATA_UPLOAD_CONTENT") + List findAllDataUploadContentInfo(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CONTENT, CONTENT_TYPE FROM LA_CARD_DATA_UPLOAD_CONTENT WHERE DIGEST = :digest") + String fileContent(); + +} diff --git a/src/main/java/io/lavagna/query/CardLabelQuery.java b/src/main/java/io/lavagna/query/CardLabelQuery.java new file mode 100644 index 000000000..a47329aab --- /dev/null +++ b/src/main/java/io/lavagna/query/CardLabelQuery.java @@ -0,0 +1,186 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.LabelAndValue; +import io.lavagna.model.LabelListValue; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +@QueryRepository +public interface CardLabelQuery { + + @Query("INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES (:projectId, :unique, :type, :domain, :name, :color)") + int addLabel(@Bind("projectId") int projectId, @Bind("unique") boolean unique, @Bind("type") String type, + @Bind("domain") String domain, @Bind("name") String name, @Bind("color") int color); + + @Query("INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER, CARD_LABEL_LIST_VALUE) VALUES (:cardLabelId, (SELECT * FROM (SELECT COALESCE(MAX(CARD_LABEL_LIST_VALUE_ORDER),0) + 1 FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_ID_FK = :cardLabelId) MAX_ORDER), :value)") + int addLabelListValue(@Bind("cardLabelId") int cardLabelId, @Bind("value") String value); + + @Query("UPDATE LA_CARD_LABEL_LIST_VALUE SET CARD_LABEL_LIST_VALUE = :value WHERE CARD_LABEL_LIST_VALUE_ID = :id") + int updateLabelListValue(@Bind("id") int id, @Bind("value") String value); + + @Query("SELECT COUNT(CARD_LABEL_VALUE_ID) FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_ID_FK = :labelId") + Integer labelUsedCount(@Bind("labelId") int labelId); + + @Query("INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES " + + " (:projectId, FALSE, 'USER', 'SYSTEM', 'ASSIGNED', 0), " + + " (:projectId, TRUE, 'TIMESTAMP', 'SYSTEM', 'DUE_DATE', 0), " + + " (:projectId, TRUE, 'LIST', 'SYSTEM', 'MILESTONE', 0), " + + " (:projectId, FALSE, 'USER', 'SYSTEM', 'WATCHED_BY', 0)") + int addSystemLabels(@Bind("projectId") int projectId); + + @Query("SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_PROJECT_ID_FK = :projectId") + List findLabelsByProject(@Bind("projectId") int projectId); + + @Query("SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = :labelId") + CardLabel findLabelById(@Bind("labelId") int labelId); + + @Query("SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = :labelName AND CARD_LABEL_DOMAIN = :labelDomain AND CARD_LABEL_PROJECT_ID_FK = :projectId") + CardLabel findLabelByName(@Bind("projectId") int projectId, @Bind("labelName") String labelName, + @Bind("labelDomain") String labelDomain); + + @Query("SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = :labelName AND CARD_LABEL_DOMAIN = :labelDomain AND CARD_LABEL_PROJECT_ID_FK = :projectId") + List findLabelsByName(@Bind("projectId") int projectId, @Bind("labelName") String labelName, + @Bind("labelDomain") String labelDomain); + + @Query("SELECT * FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_VALUE_ID = :labelValueId") + CardLabelValue findLabelValueById(@Bind("labelValueId") int labelValueId); + + @Query("SELECT * FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_ID_FK = :labelId ORDER BY CARD_LABEL_LIST_VALUE_ORDER") + List findListValuesByLabelId(@Bind("labelId") int labelId); + + @Query("SELECT * FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_ID_FK = :labelId AND CARD_LABEL_LIST_VALUE = :value ORDER BY CARD_LABEL_LIST_VALUE_ORDER") + List findListValuesByLabelIdAndValue(@Bind("labelId") int labelId, @Bind("value") String value); + + @Query("SELECT * FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE_ID = :labelListValueId") + LabelListValue findListValueById(@Bind("labelListValueId") int labelListValueId); + + @Query("UPDATE LA_CARD_LABEL SET CARD_LABEL_NAME = :name, CARD_LABEL_COLOR = :color, CARD_LABEL_TYPE = :type WHERE CARD_LABEL_ID = :cardLabelId") + int updateLabel(@Bind("name") String name, @Bind("color") int color, @Bind("type") String type, + @Bind("cardLabelId") int cardLabelId); + + @Query("DELETE FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = :labelId") + int removeLabel(@Bind("labelId") int labelId); + + @Query("DELETE FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_ID_FK = :labelId") + int removeLabelListValues(@Bind("labelId") int labelId); + + @Query("INSERT INTO LA_CARD_LABEL_VALUE(CARD_ID_FK, CARD_LABEL_VALUE_USE_UNIQUE_INDEX, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING, CARD_LABEL_VALUE_TIMESTAMP, CARD_LABEL_VALUE_INT, CARD_LABEL_VALUE_CARD_FK, CARD_LABEL_VALUE_USER_FK, CARD_LABEL_VALUE_LIST_VALUE_FK) VALUES (:cardId, :useUniqueIndex, :labelId, :valueType, :valueString, :valueTimestamp, :valueInt, :valueCard, :valueUser, :valueList)") + int addLabelValueToCard(@Bind("cardId") int cardId, @Bind("useUniqueIndex") Boolean useUniqueIndex, + @Bind("labelId") int labelId, @Bind("valueType") String valueType, @Bind("valueString") String valueString, + @Bind("valueTimestamp") Date valueTimestamp, @Bind("valueInt") Integer valueInt, + @Bind("valueCard") Integer valueCard, @Bind("valueUser") Integer valueUser, + @Bind("valueList") Integer valueList); + + @Query("DELETE FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_VALUE_ID = :cardLabelValueId") + int removeLabelValue(@Bind("cardLabelValueId") int cardLabelValueId); + + @Query("DELETE FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE_ID = :labelListValueId") + int removeLabelListValue(@Bind("labelListValueId") int labelListValueId); + + @Query("SELECT CARD_LABEL_ID, CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR, " + + " CARD_LABEL_VALUE_ID, CARD_LABEL_VALUE_USE_UNIQUE_INDEX, CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING, " + + " CARD_LABEL_VALUE_TIMESTAMP, CARD_LABEL_VALUE_INT, CARD_LABEL_VALUE_CARD_FK, CARD_LABEL_VALUE_USER_FK, CARD_LABEL_VALUE_LIST_VALUE_FK " + + " FROM LA_CARD_LABEL_VALUE INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK WHERE CARD_ID_FK = :cardId AND CARD_LABEL_VALUE_DELETED = FALSE") + List findCardLabelValuesByCardId(@Bind("cardId") int cardId); + + @Query("SELECT CARD_LABEL_ID, CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR, " + + "CARD_LABEL_VALUE_ID, CARD_LABEL_VALUE_USE_UNIQUE_INDEX, CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING, " + + "CARD_LABEL_VALUE_TIMESTAMP, CARD_LABEL_VALUE_INT, CARD_LABEL_VALUE_CARD_FK, CARD_LABEL_VALUE_USER_FK, CARD_LABEL_VALUE_LIST_VALUE_FK " + + "FROM LA_CARD_LABEL_VALUE INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK WHERE CARD_ID_FK in (:ids) AND CARD_LABEL_VALUE_DELETED = FALSE") + List findCardLabelValuesByCardIds(@Bind("ids") List ids); + + @Query("SELECT CARD_LABEL_ID, CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR, " + + "CARD_LABEL_VALUE_ID, CARD_LABEL_VALUE_USE_UNIQUE_INDEX, CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING, " + + "CARD_LABEL_VALUE_TIMESTAMP, CARD_LABEL_VALUE_INT, CARD_LABEL_VALUE_CARD_FK, CARD_LABEL_VALUE_USER_FK, CARD_LABEL_VALUE_LIST_VALUE_FK " + + "FROM LA_CARD_LABEL_VALUE INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK " + + "INNER JOIN LA_CARD ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID " + + "INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + "WHERE BOARD_COLUMN_BOARD_ID_FK = :boardId AND CARD_LABEL_VALUE_DELETED = FALSE AND BOARD_COLUMN_LOCATION = :location") + List findCardLabelValuesByBoardId(@Bind("boardId") int boardId, @Bind("location") String location); + + @Query("SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_card_label','card_label_id')))") }) + CardLabel findLastCreatedLabel(); + + @Query("SELECT * FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_VALUE_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_VALUE_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_VALUE_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_card_label_value','card_label_value_id')))") }) + CardLabelValue findLastCreatedLabelValue(); + + @Query("SELECT * FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_card_label_list_value','card_label_list_value_id')))") }) + LabelListValue findLastCreatedLabelListValue(); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_CARD_LABEL_LIST_VALUE SET CARD_LABEL_LIST_VALUE_ORDER = :order WHERE CARD_LABEL_LIST_VALUE_ID = :id") + String swapLabelListValues(); + + @Query("SELECT DISTINCT CARD_LABEL_LIST_VALUE FROM LA_CARD_LABEL_LIST_VALUE INNER JOIN LA_CARD_LABEL ON CARD_LABEL_ID_FK = CARD_LABEL_ID " + + " WHERE CARD_LABEL_DOMAIN = :domain AND CARD_LABEL_NAME = :labelName AND CARD_LABEL_LIST_VALUE LIKE CONCAT(:term, '%') ORDER BY CARD_LABEL_LIST_VALUE ASC LIMIT 20") + List findListValuesBy(@Bind("domain") String domain, @Bind("labelName") String labelName, @Bind("term") String term); + + @Query("SELECT DISTINCT CARD_LABEL_LIST_VALUE FROM LA_CARD_LABEL_LIST_VALUE INNER JOIN LA_CARD_LABEL ON CARD_LABEL_ID_FK = CARD_LABEL_ID " + + " WHERE CARD_LABEL_PROJECT_ID_FK IN (:projectIdFilter) AND CARD_LABEL_DOMAIN = :domain AND CARD_LABEL_NAME = :labelName AND CARD_LABEL_LIST_VALUE LIKE CONCAT(:term, '%') ORDER BY CARD_LABEL_LIST_VALUE ASC LIMIT 20") + List findListValuesBy(@Bind("domain") String domain, @Bind("labelName") String labelName, @Bind("term") String term, @Bind("projectIdFilter") Set projectIdFilter); + + @Query("SELECT DISTINCT CARD_LABEL_NAME FROM LA_CARD_LABEL " + + " WHERE CARD_LABEL_DOMAIN = 'USER' AND LOWER(CARD_LABEL_NAME) LIKE CONCAT(LOWER(:term), '%') ORDER BY CARD_LABEL_NAME ASC LIMIT 20") + List findUserLabelNameBy(@Bind("term") String term); + + @Query("SELECT DISTINCT CARD_LABEL_NAME FROM LA_CARD_LABEL " + + " WHERE CARD_LABEL_PROJECT_ID_FK IN (:projectIdFilter) AND CARD_LABEL_DOMAIN = 'USER' AND LOWER(CARD_LABEL_NAME) LIKE CONCAT(LOWER(:term), '%') ORDER BY CARD_LABEL_NAME ASC LIMIT 20") + List findUserLabelNameBy(@Bind("term") String term, @Bind("projectIdFilter") Set projectIdFilter); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CARD_LABEL_LIST_VALUE, CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE IN (:values)") + String findLabelListValueMapping(); + + @Query("SELECT * FROM LA_CARD_LABEL_VALUE WHERE CARD_ID_FK = :cardId AND CARD_LABEL_ID_FK = :labelId AND " + + " (CARD_LABEL_VALUE_STRING = :valueString OR (CARD_LABEL_VALUE_STRING IS NULL AND :valueString IS NULL)) AND " + + " (CARD_LABEL_VALUE_TIMESTAMP = :valueTimestamp OR (CARD_LABEL_VALUE_TIMESTAMP IS NULL AND :valueTimestamp IS NULL)) AND " + + " (CARD_LABEL_VALUE_INT = :valueInt OR (CARD_LABEL_VALUE_INT IS NULL AND :valueInt IS NULL)) AND " + + " (CARD_LABEL_VALUE_CARD_FK = :valueCard OR (CARD_LABEL_VALUE_CARD_FK IS NULL AND :valueCard IS NULL)) AND " + + " (CARD_LABEL_VALUE_USER_FK = :valueUser OR (CARD_LABEL_VALUE_USER_FK IS NULL AND :valueUser IS NULL)) AND " + + " (CARD_LABEL_VALUE_LIST_VALUE_FK = :valueList OR (CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL AND :valueList IS NULL))") + // pgsql need to have a typecast... + @QueriesOverride({ @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_LABEL_VALUE WHERE CARD_ID_FK = :cardId AND CARD_LABEL_ID_FK = :labelId AND " + + " (CARD_LABEL_VALUE_STRING = :valueString OR (CARD_LABEL_VALUE_STRING IS NULL AND :valueString IS NULL)) AND " + + " (CARD_LABEL_VALUE_TIMESTAMP = :valueTimestamp OR (CARD_LABEL_VALUE_TIMESTAMP IS NULL AND :valueTimestamp::timestamp IS NULL)) AND " + + " (CARD_LABEL_VALUE_INT = :valueInt OR (CARD_LABEL_VALUE_INT IS NULL AND :valueInt IS NULL)) AND " + + " (CARD_LABEL_VALUE_CARD_FK = :valueCard OR (CARD_LABEL_VALUE_CARD_FK IS NULL AND :valueCard IS NULL)) AND " + + " (CARD_LABEL_VALUE_USER_FK = :valueUser OR (CARD_LABEL_VALUE_USER_FK IS NULL AND :valueUser IS NULL)) AND " + + " (CARD_LABEL_VALUE_LIST_VALUE_FK = :valueList OR (CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL AND :valueList IS NULL))") }) + List findLabelValueByLabelAndValue(@Bind("cardId") int cardId, @Bind("labelId") int labelId, + @Bind("valueString") String valueString, @Bind("valueTimestamp") Date valueTimestamp, + @Bind("valueInt") Integer valueInt, @Bind("valueCard") Integer valueCard, + @Bind("valueUser") Integer valueUser, @Bind("valueList") Integer valueList); +} diff --git a/src/main/java/io/lavagna/query/CardQuery.java b/src/main/java/io/lavagna/query/CardQuery.java new file mode 100644 index 000000000..3257dd9f3 --- /dev/null +++ b/src/main/java/io/lavagna/query/CardQuery.java @@ -0,0 +1,161 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.Card; +import io.lavagna.model.CardFull; +import io.lavagna.model.Event; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +@QueryRepository +public interface CardQuery { + + @Query("INSERT INTO LA_CARD(CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER, CARD_LAST_UPDATED, CARD_LAST_UPDATED_USER_ID_FK) VALUES " + + " (:name, :columnId, (SELECT * FROM (SELECT COALESCE(MAX(CARD_ORDER), 0) + 1 FROM LA_CARD WHERE CARD_BOARD_COLUMN_ID_FK = :columnId) AS MAX_CARD_ORDER), :userId, :cardSequence, NOW(), :userId)") + int createCard(@Bind("name") String name, @Bind("columnId") int columnId, @Bind("userId") int userId, + @Bind("cardSequence") int cardSequence); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_USER_ID_FK, CARD_ORDER, CARD_SEQ_NUMBER FROM LA_CARD_WITH_BOARD_ID WHERE BOARD_ID = :boardId AND " + + " BOARD_COLUMN_LOCATION = :location " + " ORDER BY CARD_ORDER ASC, CARD_NAME ASC") + List findAllByBoardIdAndLocation(@Bind("boardId") int boardId, @Bind("location") String location); + + @Query("SELECT CARD_ID FROM LA_CARD WHERE CARD_ID IN (:cardIds) AND CARD_BOARD_COLUMN_ID_FK = :columnId") + List findCardIdsInColumnId(@Bind("cardIds") List cardIds, @Bind("columnId") int columnId); + + @Query("SELECT * FROM LA_CARD_FULL WHERE BOARD_SHORT_NAME = :boardShortName") + List findAllByBoardShortName(@Bind("boardShortName") String boardShortName); + + @Query("SELECT * FROM LA_CARD_FULL WHERE (LOWER(CARD_NAME) LIKE CONCAT('%', CONCAT(LOWER(:term), '%')) OR CARD_SEQ_NUMBER LIKE CONCAT(:term, '%')) AND PROJECT_ID IN (:projectIdFilter) ORDER BY BOARD_SHORT_NAME ASC, CARD_SEQ_NUMBER ASC LIMIT 10") + @QueriesOverride({ + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_FULL WHERE (LOWER(CARD_NAME) LIKE CONCAT('%', CONCAT(LOWER(:term), '%')) OR CAST(CARD_SEQ_NUMBER AS TEXT) LIKE CONCAT(:term, '%')) AND PROJECT_ID IN (:projectIdFilter) ORDER BY BOARD_SHORT_NAME ASC, CARD_SEQ_NUMBER ASC LIMIT 10") + }) + List findCardBy(@Bind("term") String term, @Bind("projectIdFilter") Set projectIdFilter); + + @Query("SELECT * FROM LA_CARD_FULL WHERE (LOWER(CARD_NAME) LIKE CONCAT('%', CONCAT(LOWER(:term), '%')) OR CARD_SEQ_NUMBER LIKE CONCAT(:term, '%')) ORDER BY BOARD_SHORT_NAME ASC, CARD_SEQ_NUMBER ASC LIMIT 10") + @QueriesOverride({ + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_CARD_FULL WHERE (LOWER(CARD_NAME) LIKE CONCAT('%', CONCAT(LOWER(:term), '%')) OR CAST(CARD_SEQ_NUMBER AS TEXT) LIKE CONCAT(:term, '%')) ORDER BY BOARD_SHORT_NAME ASC, CARD_SEQ_NUMBER ASC LIMIT 10") + }) + List findCardBy(@Bind("term") String term); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_SEQ_NUMBER, CARD_USER_ID_FK FROM LA_CARD " + + " INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + " WHERE " + + " BOARD_COLUMN_BOARD_ID_FK = :boardId AND " + + " BOARD_COLUMN_LOCATION = :location " + + " ORDER BY CARD_LAST_UPDATED DESC " + " LIMIT :amount OFFSET :offset ") + List fetchPaginatedByBoardIdAndLocation(@Bind("boardId") int boardId, @Bind("location") String location, + @Bind("amount") int amount, @Bind("offset") int offset); + + @Query("SELECT * FROM LA_CARD_FULL WHERE CARD_BOARD_COLUMN_ID_FK = :columnId ORDER BY CARD_ORDER ASC, CARD_NAME ASC") + List findAllFullByColumnId(@Bind("columnId") int columnId); + + @Query("SELECT * FROM LA_CARD_FULL WHERE CARD_ID IN (:ids)") + List findAllByIds(@Bind("ids") Collection ids); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, " + + "CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_SHORT_NAME, PROJECT_SHORT_NAME FROM LA_ASSIGNED_CARD " + + "INNER JOIN LA_CARD_FULL ON ASSIGNED_CARD_ID = CARD_ID " + + "WHERE ASSIGNED_USER_ID = :userId AND BOARD_COLUMN_DEFINITION_VALUE = ASSIGNED_CARD_STATUS AND BOARD_COLUMN_DEFINITION_VALUE = 'OPEN' " + + "ORDER BY ASSIGNED_EVENT_TIME DESC LIMIT :amount OFFSET :offset") + List fetchAllOpenCardsByUserId(@Bind("userId") int userId, @Bind("amount") int amount, + @Bind("offset") int offset); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, " + + " CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_SHORT_NAME, PROJECT_SHORT_NAME FROM LA_ASSIGNED_CARD " + + " INNER JOIN LA_CARD_FULL ON ASSIGNED_CARD_ID = CARD_ID " + + " WHERE ASSIGNED_USER_ID = :userId AND BOARD_COLUMN_DEFINITION_VALUE = ASSIGNED_CARD_STATUS AND BOARD_COLUMN_DEFINITION_VALUE = 'OPEN' " + + " AND PROJECT_SHORT_NAME = :projectShortName ORDER BY ASSIGNED_EVENT_TIME DESC LIMIT :amount OFFSET :offset") + List fetchAllOpenCardsByProjectIdAndUserId(@Bind("userId") int userId, + @Bind("projectShortName") String projectShortName, @Bind("amount") int amount, @Bind("offset") int offset); + + @Query("SELECT COUNT(LA_ASSIGNED_CARD.ASSIGNED_CARD_ID) FROM LA_ASSIGNED_CARD WHERE ASSIGNED_USER_ID = :userId AND ASSIGNED_CARD_STATUS = 'OPEN'") + Integer getOpenCardsCountByUserId(@Bind("userId") int userId); + + @Query("SELECT COUNT(LA_ASSIGNED_CARD_PROJECT.ASSIGNED_CARD_ID) FROM LA_ASSIGNED_CARD_PROJECT WHERE ASSIGNED_USER_ID = :userId AND ASSIGNED_CARD_STATUS = 'OPEN' AND ASSIGNED_PROJECT_SHORT_NAME = :projectShortName") + Integer getOpenCardsCountByProjectAndUserId(@Bind("projectShortName") String projectShortName, + @Bind("userId") int userId); + + @Query("SELECT CARD_ID, CARD_NAME,CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER FROM LA_CARD_WITH_BOARD_ID WHERE BOARD_ID = :boardId AND CARD_NAME LIKE CONCAT('%', :criteria,'%') ORDER BY CARD_NAME") + List findCards(@Bind("boardId") int boardId, @Bind("criteria") String criteria); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER FROM LA_CARD WHERE CARD_ID = :cardId") + Card findBy(@Bind("cardId") int cardId); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_SHORT_NAME, PROJECT_SHORT_NAME FROM LA_CARD_FULL WHERE CARD_ID = :cardId") + CardFull findFullBy(@Bind("cardId") int cardId); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_SHORT_NAME, PROJECT_SHORT_NAME FROM LA_CARD_FULL " + + " WHERE CARD_SEQ_NUMBER = :seqNumber AND BOARD_SHORT_NAME = :shortName") + CardFull findFullBy(@Bind("shortName") String shortName, @Bind("seqNumber") int seqNumber); + + @Query("SELECT CARD_ID FROM LA_CARD " + + " INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK " + + " INNER JOIN LA_BOARD ON LA_BOARD.BOARD_ID = LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK WHERE " + + " LA_CARD.CARD_SEQ_NUMBER = :seqNumber AND LA_BOARD.BOARD_SHORT_NAME = :shortName") + Integer findCardIdByBoardNameAndSeq(@Bind("shortName") String shortName, @Bind("seqNumber") int seqNumber); + + + @Query("SELECT COUNT(CARD_ID) FROM LA_CARD " + + " INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK " + + " INNER JOIN LA_BOARD ON LA_BOARD.BOARD_ID = LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK WHERE " + + " LA_CARD.CARD_SEQ_NUMBER = :seqNumber AND LA_BOARD.BOARD_SHORT_NAME = :shortName") + Integer countCardIdByBoardNameAndSeq(@Bind("shortName") String shortName, @Bind("seqNumber") int seqNumber); + + @Query("UPDATE LA_CARD SET CARD_NAME = :name WHERE CARD_ID = :cardId") + int updateCard(@Bind("name") String name, @Bind("cardId") int cardId); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER FROM LA_CARD WHERE CARD_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER FROM LA_CARD WHERE CARD_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER FROM LA_CARD WHERE CARD_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_card','card_id')))") }) + Card findLastCreatedCard(); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_CARD SET CARD_BOARD_COLUMN_ID_FK = :columnId WHERE CARD_ID = :cardId AND CARD_BOARD_COLUMN_ID_FK = :previousColumnId") + String moveCardToColumn(); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_CARD SET CARD_ORDER = :cardOrder WHERE CARD_ID = :cardId AND CARD_BOARD_COLUMN_ID_FK = :columnId") + String updateCardOrder(); + + @Query("UPDATE LA_CARD SET CARD_ORDER = :order WHERE CARD_ID = :cardId") + int updateCardOrder(@Bind("cardId") int cardId, @Bind("order") int order); + + @Query("UPDATE LA_CARD SET CARD_ORDER = CARD_ORDER + 1 WHERE CARD_BOARD_COLUMN_ID_FK = :columnId") + int incrementCardsOrder(@Bind("columnId") int columnId); + + @Query("SELECT BOARD_COUNTER_CARD_SEQUENCE FROM LA_BOARD_COUNTER WHERE BOARD_COUNTER_ID_FK = (SELECT BOARD_COLUMN_BOARD_ID_FK FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID = :columnId) FOR UPDATE") + Integer fetchAndLockCardSequence(@Bind("columnId") int columnId); + + @Query("UPDATE LA_BOARD_COUNTER SET BOARD_COUNTER_CARD_SEQUENCE = BOARD_COUNTER_CARD_SEQUENCE + 1 WHERE BOARD_COUNTER_CARD_SEQUENCE = :expectedSequenceValue AND BOARD_COUNTER_ID_FK = (SELECT BOARD_COLUMN_BOARD_ID_FK FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID = :columnId)") + int incrementSequence(@Bind("expectedSequenceValue") int expectedSequenceValue, @Bind("columnId") int columnId); + + @Query("SELECT * FROM LA_EVENT WHERE EVENT_CARD_ID_FK = :cardId ORDER BY EVENT_TIME DESC") + List fetchAllActivityByCardId(@Bind("cardId") int cardId); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CONCAT(CONCAT(BOARD_SHORT_NAME, '-'), CARD_SEQ_NUMBER) AS CARD_IDENTIFIER, CARD_ID FROM LA_CARD " + + " INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + " INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE (BOARD_SHORT_NAME, CARD_SEQ_NUMBER) IN (:projShortNameAndCardSeq)") + String findCardsIs(); +} diff --git a/src/main/java/io/lavagna/query/ConfigurationQuery.java b/src/main/java/io/lavagna/query/ConfigurationQuery.java new file mode 100644 index 000000000..967c3e9ed --- /dev/null +++ b/src/main/java/io/lavagna/query/ConfigurationQuery.java @@ -0,0 +1,50 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.Query; +import io.lavagna.common.QueryRepository; +import io.lavagna.model.ConfigurationKeyValue; + +import java.util.List; +import java.util.Set; + +@QueryRepository +public interface ConfigurationQuery { + + @Query("SELECT COUNT(*) FROM LA_CONF WHERE CONF_KEY = :key") + Integer hasKeyDefined(@Bind("key") String key); + + @Query("SELECT * FROM LA_CONF WHERE CONF_KEY IN (:keys)") + List findConfigurationFor(@Bind("keys") Set keys); + + @Query("SELECT CONF_VALUE FROM LA_CONF WHERE CONF_KEY = :key") + List getValue(@Bind("key") String key); + + @Query("INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES(:key, :value)") + int set(@Bind("key") String key, @Bind("value") String value); + + @Query("UPDATE LA_CONF SET CONF_VALUE = :value WHERE CONF_KEY = :key") + int update(@Bind("key") String key, @Bind("value") String value); + + @Query("SELECT * FROM LA_CONF ORDER BY CONF_KEY") + List findAll(); + + @Query("DELETE FROM LA_CONF WHERE CONF_KEY = :key") + int delete(@Bind("key") String key); +} diff --git a/src/main/java/io/lavagna/query/DB.java b/src/main/java/io/lavagna/query/DB.java new file mode 100644 index 000000000..b22300987 --- /dev/null +++ b/src/main/java/io/lavagna/query/DB.java @@ -0,0 +1,22 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +public class DB { + public static final String MYSQL = "MYSQL"; + public static final String PGSQL = "PGSQL"; +} diff --git a/src/main/java/io/lavagna/query/EventQuery.java b/src/main/java/io/lavagna/query/EventQuery.java new file mode 100644 index 000000000..523fce92d --- /dev/null +++ b/src/main/java/io/lavagna/query/EventQuery.java @@ -0,0 +1,125 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.Event; +import io.lavagna.model.EventsCount; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@QueryRepository +public interface EventQuery { + + @Query("SELECT * FROM LA_EVENT WHERE EVENT_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_EVENT WHERE EVENT_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_EVENT WHERE EVENT_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_event','event_id')))") }) + Event findLastCreated(); + + @Query("SELECT * FROM LA_EVENT WHERE EVENT_ID = :id") + Event getById(@Bind("id") int id); + + @Query("SELECT * FROM LA_EVENT ORDER BY EVENT_ID ASC LIMIT :amount OFFSET :offset ") + List find(@Bind("offset") int offset, @Bind("amount") int amount); + + @Query("SELECT COUNT(EVENT_ID) FROM LA_EVENT") + Integer count(); + + @Query("SELECT * FROM LA_EVENT WHERE EVENT_CARD_DATA_ID_FK = :cardDataId AND EVENT_ID > :eventId AND EVENT_TYPE = :eventType ORDER BY EVENT_ID ASC LIMIT 1") + List findNextEventFor(@Bind("cardDataId") int cardDataId, @Bind("eventId") int eventId, + @Bind("eventType") String eventType); + + @Query("INSERT INTO LA_EVENT(EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TIME, EVENT_TYPE, EVENT_VALUE_INT, EVENT_VALUE_STRING, EVENT_VALUE_TIMESTAMP, EVENT_VALUE_CARD_FK, EVENT_VALUE_USER_FK) " + + " VALUES (:labelName, :labelType, :cardId, :userId, :now, :event, :valueInt, :valueString, :valueTimestamp, :valueCard, :valueUser)") + int insertLabelEvent(@Bind("labelName") String labelName, @Bind("labelType") String labelType, + @Bind("cardId") int cardId, @Bind("userId") int userId, @Bind("now") Date now, @Bind("event") String event, + @Bind("valueInt") Integer valueInt, @Bind("valueString") String valueString, + @Bind("valueTimestamp") Date valueTimestamp, @Bind("valueCard") Integer valueCard, + @Bind("valueUser") Integer valueUser); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_EVENT(EVENT_CARD_ID_FK, EVENT_PREV_COLUMN_ID_FK, EVENT_COLUMN_ID_FK, EVENT_USER_ID_FK, EVENT_TIME, EVENT_TYPE, EVENT_VALUE_STRING) " + + " VALUES (:cardId, :previousColumnId, :columnId, :userId, :time, :event, :valueString)") + String insertCardEvent(); + + @Query("INSERT INTO LA_EVENT(EVENT_CARD_DATA_ID_FK, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TIME, EVENT_TYPE, EVENT_PREV_CARD_DATA_ID_FK, EVENT_NEW_CARD_DATA_ID_FK) " + + " VALUES (:cardDataId, :cardId, :userId, :time, :event, :referenceId, :newReferenceId)") + int insertCardDataEvent(@Bind("cardDataId") int cardDataId, @Bind("cardId") int cardId, @Bind("userId") int userId, + @Bind("time") Date time, @Bind("event") String event, @Bind("referenceId") Integer referenceId, + @Bind("newReferenceId") Integer newReferenceId); + + @Query("INSERT INTO LA_EVENT(EVENT_CARD_DATA_ID_FK, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TIME, EVENT_TYPE, EVENT_PREV_CARD_DATA_ID_FK, EVENT_VALUE_STRING) " + + " VALUES (:cardDataId, :cardId, :userId, :time, :event, :referenceId, :name)") + int insertFileEvent(@Bind("cardDataId") int cardDataId, @Bind("cardId") int cardId, @Bind("userId") int userId, + @Bind("time") Date time, @Bind("event") String event, @Bind("referenceId") Integer referenceId, + @Bind("name") String name); + + @Query("SELECT EVENT_USER_ID_FK FROM LA_EVENT WHERE EVENT_CARD_DATA_ID_FK = :cardDataId AND EVENT_TYPE = :event") + List findUsersIdForCardData(@Bind("cardDataId") int cardDataId, @Bind("event") String event); + + @Query("DELETE FROM LA_EVENT WHERE EVENT_ID = :id AND EVENT_CARD_ID_FK = :cardId AND EVENT_TYPE = :event") + int remove(@Bind("id") int id, @Bind("cardId") int cardId, @Bind("event") String event); + + @Query("SELECT EVENT_ID, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_CARD_DATA_ID_FK, " + + " EVENT_PREV_CARD_DATA_ID_FK, EVENT_NEW_CARD_DATA_ID_FK, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK, EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_VALUE_INT, " + + " EVENT_VALUE_STRING, EVENT_VALUE_TIMESTAMP, EVENT_VALUE_CARD_FK, EVENT_VALUE_USER_FK " + + " FROM LA_EVENT INNER JOIN LA_CARD ON LA_EVENT.EVENT_CARD_ID_FK = LA_CARD.CARD_ID WHERE LA_CARD.CARD_USER_ID_FK = :user " + + " UNION " + + " SELECT EVENT_ID, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_CARD_DATA_ID_FK, EVENT_PREV_CARD_DATA_ID_FK, EVENT_NEW_CARD_DATA_ID_FK, " + + " EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK, EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_VALUE_INT, EVENT_VALUE_STRING, " + + " EVENT_VALUE_TIMESTAMP, EVENT_VALUE_CARD_FK, EVENT_VALUE_USER_FK " + + " FROM LA_EVENT INNER JOIN LA_CARD_LABEL_VALUE ON LA_EVENT.EVENT_CARD_ID_FK = LA_CARD_LABEL_VALUE.CARD_ID_FK " + + " WHERE LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_USER_FK = :user " + + " ORDER BY EVENT_TIME DESC LIMIT :amount OFFSET :offset ") + List getUserFeedByPage(@Bind("user") int user, @Bind("amount") int amount, @Bind("offset") int offset); + + // profile + + @Query("SELECT EVENT_ID, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_CARD_DATA_ID_FK," + + " EVENT_PREV_CARD_DATA_ID_FK, EVENT_NEW_CARD_DATA_ID_FK, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK, EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_VALUE_INT," + + " EVENT_VALUE_STRING, EVENT_VALUE_TIMESTAMP, EVENT_VALUE_CARD_FK, EVENT_VALUE_USER_FK FROM LA_EVENT" + + " WHERE EVENT_USER_ID_FK = :userId ORDER BY EVENT_TIME DESC LIMIT :amount OFFSET :offset") + List getLatestActivityByPage(@Bind("userId") int user, @Bind("amount") int amount, @Bind("offset") int offset); + + @Query("SELECT EVENT_ID, EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_CARD_DATA_ID_FK," + + " EVENT_PREV_CARD_DATA_ID_FK, EVENT_NEW_CARD_DATA_ID_FK, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK, EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_VALUE_INT," + + " EVENT_VALUE_STRING, EVENT_VALUE_TIMESTAMP, EVENT_VALUE_CARD_FK, EVENT_VALUE_USER_FK" + + " FROM LA_EVENT INNER JOIN LA_CARD ON LA_EVENT.EVENT_CARD_ID_FK = LA_CARD.CARD_ID" + + " INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK" + + " INNER JOIN LA_BOARD ON LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID AND LA_BOARD.BOARD_PROJECT_ID_FK in (:projects)" + + " WHERE EVENT_USER_ID_FK = :userId ORDER BY EVENT_TIME DESC LIMIT :amount OFFSET :offset") + List getLatestActivityByPageAndProjects(@Bind("userId") int user, + @Bind("projects") Collection projects, @Bind("amount") int amount, @Bind("offset") int offset); + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + " WHERE EVENT_USER_ID_FK = :userId AND EVENT_TIME >= :fromDate GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getUserActivity(@Bind("userId") int userId, @Bind("fromDate") Date fromDate); + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + " INNER JOIN LA_CARD_FULL ON EVENT_CARD_ID_FK = LA_CARD_FULL.CARD_ID AND PROJECT_ID IN (:projectIds) " + + " WHERE EVENT_USER_ID_FK = :userId AND EVENT_TIME >= :fromDate GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getUserActivityByProjects(@Bind("userId") int userId, + @Bind("projectIds") Collection projectIds, @Bind("fromDate") Date fromDate); + +} diff --git a/src/main/java/io/lavagna/query/MySqlFullTextSupportQuery.java b/src/main/java/io/lavagna/query/MySqlFullTextSupportQuery.java new file mode 100644 index 000000000..64a951d4c --- /dev/null +++ b/src/main/java/io/lavagna/query/MySqlFullTextSupportQuery.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Query; +import io.lavagna.common.QueryRepository; + +@QueryRepository +public interface MySqlFullTextSupportQuery { + + @Query("INSERT INTO LA_CARD_FTS_SUPPORT SELECT CARD_ID,CARD_NAME,CARD_LAST_UPDATED FROM LA_CARD left join LA_CARD_FTS_SUPPORT ON CARD_ID = CARD_FTS_SUPPORT_CARD_ID_FK WHERE CARD_FTS_SUPPORT_CARD_ID_FK IS NULL LIMIT 1000") + int syncNewCards(); + + @Query("REPLACE INTO LA_CARD_FTS_SUPPORT SELECT CARD_ID,CARD_NAME,CARD_LAST_UPDATED FROM LA_CARD left join LA_CARD_FTS_SUPPORT ON CARD_ID = CARD_FTS_SUPPORT_CARD_ID_FK WHERE CARD_LAST_UPDATED <> CARD_FTS_SUPPORT_LAST_UPDATED LIMIT 1000") + int syncUpdatedCards(); + + @Query("INSERT INTO LA_CARD_DATA_FTS_SUPPORT SELECT CARD_DATA_ID,CARD_DATA_CONTENT,CARD_DATA_LAST_UPDATED FROM LA_CARD_DATA left join LA_CARD_DATA_FTS_SUPPORT ON CARD_DATA_ID = CARD_DATA_FTS_SUPPORT_CARD_DATA_ID_FK WHERE CARD_DATA_FTS_SUPPORT_CARD_DATA_ID_FK IS NULL LIMIT 1000") + int syncNewCardData(); + + @Query("REPLACE INTO LA_CARD_DATA_FTS_SUPPORT SELECT CARD_DATA_ID,CARD_DATA_CONTENT,CARD_DATA_LAST_UPDATED FROM LA_CARD_DATA left join LA_CARD_DATA_FTS_SUPPORT ON CARD_DATA_ID = CARD_DATA_FTS_SUPPORT_CARD_DATA_ID_FK WHERE CARD_DATA_LAST_UPDATED <> CARD_DATA_FTS_SUPPORT_LAST_UPDATED LIMIT 1000") + int syncUpdatedCardData(); +} diff --git a/src/main/java/io/lavagna/query/NotificationQuery.java b/src/main/java/io/lavagna/query/NotificationQuery.java new file mode 100644 index 000000000..22f251d74 --- /dev/null +++ b/src/main/java/io/lavagna/query/NotificationQuery.java @@ -0,0 +1,66 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.Query; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.Event; + +import java.util.Date; +import java.util.List; + +@QueryRepository +public interface NotificationQuery { + + @Query(type = QueryType.TEMPLATE, value = "SELECT COUNT(EVENT_ID) COUNT_EVENT_ID, USER_ID FROM LA_EVENT INNER JOIN (SELECT USER_ID, CARD_ID_FK, USER_LAST_CHECKED FROM LA_CARD_LABEL " + + " INNER JOIN LA_CARD_LABEL_VALUE ON CARD_LABEL_ID = CARD_LABEL_ID_FK " + + " INNER JOIN LA_USER ON CARD_LABEL_VALUE_USER_FK = USER_ID " + + " WHERE CARD_LABEL_DOMAIN = 'SYSTEM' AND CARD_LABEL_NAME IN ('ASSIGNED', 'WATCHED_BY')) CARD_FOR_USER " + + " ON CARD_ID_FK = EVENT_CARD_ID_FK " + + " WHERE USER_LAST_CHECKED IS NULL OR EVENT_TIME >= USER_LAST_CHECKED GROUP BY USER_ID HAVING COUNT(EVENT_ID) > 0") + String countNewForUsersId(); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_USER SET USER_LAST_CHECKPOINT_COUNT = USER_LAST_CHECKPOINT_COUNT + :count WHERE USER_ID = :userId") + String updateCount(); + + @Query("UPDATE LA_USER SET USER_LAST_CHECKED = :checkDate") + int updateCheckDate(@Bind("checkDate") Date checkDate); + + @Query(type = QueryType.TEMPLATE, value = "SELECT USER_ID FROM LA_USER WHERE USER_LAST_CHECKPOINT_COUNT > 0") + String usersToNotify(); + + @Query(type = QueryType.TEMPLATE, value = "UPDATE LA_USER SET USER_LAST_CHECKPOINT_COUNT = 0 WHERE USER_LAST_CHECKPOINT_COUNT > 0 ") + String reset(); + + @Query(type = QueryType.TEMPLATE, value = " AND USER_ID NOT IN (:userWithChanges) ") + String notIn(); + + @Query("SELECT USER_LAST_EMAIL_SENT FROM LA_USER WHERE USER_ID = :userId") + Date lastEmailSent(@Bind("userId") int userId); + + @Query("SELECT * FROM (SELECT DISTINCT CARD_ID_FK FROM LA_CARD_LABEL " + + " INNER JOIN LA_CARD_LABEL_VALUE ON CARD_LABEL_ID = CARD_LABEL_ID_FK " + + " WHERE CARD_LABEL_VALUE_USER_FK = :userId AND CARD_LABEL_DOMAIN = 'SYSTEM' AND " + + " CARD_LABEL_NAME IN ('ASSIGNED', 'WATCHED_BY') ) CARD_IDS " + + " INNER JOIN LA_EVENT ON CARD_ID_FK = EVENT_CARD_ID_FK WHERE EVENT_TIME BETWEEN :from AND :upTo ORDER BY EVENT_TIME ASC") + List eventsForUser(@Bind("userId") int userId, @Bind("from") Date from, @Bind("upTo") Date upTo); + + @Query("UPDATE LA_USER SET USER_LAST_EMAIL_SENT = :sentDate WHERE USER_ID = :userId") + int updateSentEmailDate(@Bind("sentDate") Date sentDate, @Bind("userId") int userId); +} diff --git a/src/main/java/io/lavagna/query/PermissionQuery.java b/src/main/java/io/lavagna/query/PermissionQuery.java new file mode 100644 index 000000000..fd1161fb2 --- /dev/null +++ b/src/main/java/io/lavagna/query/PermissionQuery.java @@ -0,0 +1,140 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.Query; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.ProjectRoleAndPermission; +import io.lavagna.model.RoleAndMetadata; +import io.lavagna.model.RoleAndPermission; +import io.lavagna.model.User; +import io.lavagna.model.UserIdentifier; + +import java.util.List; + +@QueryRepository +public interface PermissionQuery { + + @Query("SELECT ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY, PERMISSION FROM LA_USER_ROLE " + + " INNER JOIN LA_ROLE ON LA_USER_ROLE.ROLE_ID_FK = ROLE_ID " + + " LEFT JOIN LA_ROLE_PERMISSION ON LA_ROLE_PERMISSION.ROLE_ID_FK = ROLE_ID " + + " WHERE USER_ID_FK = :userId") + List findBaseRoleAndPermissionByUserId(@Bind("userId") int userId); + + @Query("SELECT PROJECT_ROLE_NAME AS ROLE_NAME, PROJECT_ROLE_REMOVABLE AS ROLE_REMOVABLE, PROJECT_ROLE_HIDDEN AS ROLE_HIDDEN, PROJECT_ROLE_READONLY AS ROLE_READONLY, " + + " PERMISSION FROM LA_PROJECT_USER_ROLE " + + " INNER JOIN LA_PROJECT_ROLE ON LA_PROJECT_USER_ROLE.PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID " + + " LEFT JOIN LA_PROJECT_ROLE_PERMISSION ON LA_PROJECT_ROLE_PERMISSION.PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID " + + " WHERE USER_ID_FK = :userId AND LA_PROJECT_USER_ROLE.PROJECT_ID_FK = :projectId AND LA_PROJECT_ROLE.PROJECT_ID_FK = :projectId") + List findRoleAndPermissionByUserIdInProjectId(@Bind("userId") int userId, + @Bind("projectId") int projectId); + + @Query("SELECT LA_PROJECT.PROJECT_ID AS PROJECT_ID, LA_PROJECT.PROJECT_SHORT_NAME AS PROJECT_SHORT_NAME, PROJECT_ROLE_NAME AS ROLE_NAME, PROJECT_ROLE_REMOVABLE AS ROLE_REMOVABLE, PERMISSION FROM LA_PROJECT_USER_ROLE " + + " INNER JOIN LA_PROJECT_ROLE ON LA_PROJECT_USER_ROLE.PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID " + + " INNER JOIN LA_PROJECT ON LA_PROJECT_ROLE.PROJECT_ID_FK = LA_PROJECT.PROJECT_ID " + + " LEFT JOIN LA_PROJECT_ROLE_PERMISSION ON LA_PROJECT_ROLE_PERMISSION.PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID " + + " WHERE USER_ID_FK = :userId") + List findPermissionsGroupedByProjectForUserId(@Bind("userId") int userId); + + @Query("SELECT ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY, PERMISSION from LA_ROLE_PERMISSION RIGHT JOIN LA_ROLE ON ROLE_ID = ROLE_ID_FK") + List findAllRolesAndRelatedPermission(); + + @Query("SELECT ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY FROM LA_ROLE WHERE ROLE_NAME = :name") + RoleAndMetadata findRoleByName(@Bind("name") String name); + + @Query("SELECT PROJECT_ROLE_NAME AS ROLE_NAME, PROJECT_ROLE_REMOVABLE AS ROLE_REMOVABLE, PROJECT_ROLE_HIDDEN AS ROLE_HIDDEN, PROJECT_ROLE_READONLY AS ROLE_READONLY," + + " PERMISSION from LA_PROJECT_ROLE_PERMISSION RIGHT JOIN LA_PROJECT_ROLE ON PROJECT_ROLE_ID = PROJECT_ROLE_ID_FK WHERE PROJECT_ID_FK = :projectId") + List findAllRolesAndRelatedPermissionInProjectId(@Bind("projectId") int projectId); + + @Query("SELECT PROJECT_ROLE_NAME AS ROLE_NAME, PROJECT_ROLE_REMOVABLE AS ROLE_REMOVABLE, PROJECT_ROLE_HIDDEN AS ROLE_HIDDEN, PROJECT_ROLE_READONLY AS ROLE_READONLY " + + " from LA_PROJECT_ROLE WHERE PROJECT_ID_FK = :projectId AND PROJECT_ROLE_NAME = :name") + RoleAndMetadata findRoleInProjectIdByName(@Bind("projectId") int projectId, + @Bind("name") String name); + + @Query("INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE) VALUES (:roleName, TRUE)") + int createRole(@Bind("roleName") String roleName); + + @Query("INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES (:roleName, :removable, :hidden, :readOnly)") + int createFullRole(@Bind("roleName") String roleName, @Bind("removable") boolean removable, + @Bind("hidden") boolean hidden, @Bind("readOnly") boolean readOnly); + + @Query("INSERT INTO LA_PROJECT_ROLE(PROJECT_ROLE_NAME, PROJECT_ID_FK) VALUES (:roleName, :projectId)") + int createRoleInProjectId(@Bind("roleName") String roleName, @Bind("projectId") int projectId); + + @Query("INSERT INTO LA_PROJECT_ROLE(PROJECT_ROLE_NAME, PROJECT_ID_FK, PROJECT_ROLE_REMOVABLE, PROJECT_ROLE_HIDDEN, PROJECT_ROLE_READONLY) " + + " VALUES (:roleName, :projectId, :removable, :hidden, :readOnly)") + int createFullRoleInProjectId(@Bind("roleName") String roleName, @Bind("projectId") int projectId, + @Bind("removable") boolean removable, @Bind("hidden") boolean hidden, @Bind("readOnly") boolean readOnly); + + @Query("DELETE FROM LA_ROLE WHERE ROLE_NAME = :roleName AND ROLE_REMOVABLE = TRUE") + int deleteRole(@Bind("roleName") String roleName); + + @Query("DELETE FROM LA_PROJECT_ROLE WHERE PROJECT_ROLE_NAME = :roleName AND PROJECT_ID_FK = :projectId") + int deleteRoleInProjectId(@Bind("roleName") String roleName, @Bind("projectId") int projectId); + + @Query("DELETE FROM LA_ROLE_PERMISSION WHERE ROLE_ID_FK = (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = :roleName)") + int deletePermissions(@Bind("roleName") String roleName); + + @Query("DELETE FROM LA_PROJECT_ROLE_PERMISSION WHERE PROJECT_ROLE_ID_FK = (SELECT PROJECT_ROLE_ID FROM LA_PROJECT_ROLE WHERE PROJECT_ROLE_NAME = :roleName AND PROJECT_ID_FK = :projectId)") + int deletePermissionsInProjectId(@Bind("roleName") String roleName, @Bind("projectId") int projectId); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = :roleName), :permission)") + String addPermission(); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_PROJECT_ROLE_PERMISSION(PROJECT_ROLE_ID_FK, PERMISSION) VALUES ((SELECT PROJECT_ROLE_ID FROM LA_PROJECT_ROLE WHERE PROJECT_ROLE_NAME = :roleName AND PROJECT_ID_FK = :projectId), :permission)") + String addPermissionInProjectId(); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES ((:userId), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = :roleName))") + String assignRoleToUser(); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_PROJECT_USER_ROLE(PROJECT_ID_FK, USER_ID_FK, PROJECT_ROLE_ID_FK) VALUES (:projectId, (:userId), (SELECT PROJECT_ROLE_ID FROM LA_PROJECT_ROLE WHERE PROJECT_ROLE_NAME = :roleName AND PROJECT_ID_FK = :projectId))") + String assignRoleToUsersInProjectId(); + + @Query(type = QueryType.TEMPLATE, value = "DELETE FROM LA_USER_ROLE WHERE USER_ID_FK = (:userId) AND ROLE_ID_FK = (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = :roleName)") + String removeRoleToUsers(); + + @Query(type = QueryType.TEMPLATE, value = "DELETE FROM LA_PROJECT_USER_ROLE WHERE USER_ID_FK = (:userId) AND PROJECT_ROLE_ID_FK = (SELECT PROJECT_ROLE_ID FROM LA_PROJECT_ROLE WHERE PROJECT_ROLE_NAME = :roleName AND PROJECT_ID_FK = :projectId) AND PROJECT_ID_FK = :projectId") + String removeRoleToUsersInProjectId(); + + @Query("SELECT USER_ID, USER_PROVIDER, USER_NAME, USER_EMAIL, USER_DISPLAY_NAME, USER_ENABLED, USER_EMAIL_NOTIFICATION, USER_MEMBER_SINCE FROM LA_USER " + + " INNER JOIN LA_USER_ROLE ON USER_ID = USER_ID_FK " + + " INNER JOIN LA_ROLE ON ROLE_ID_FK = ROLE_ID WHERE ROLE_NAME = :roleName ORDER BY USER_PROVIDER, USER_NAME") + List findUserByRole(@Bind("roleName") String roleName); + + @Query("SELECT USER_PROVIDER, USER_NAME FROM LA_USER " + + " INNER JOIN LA_USER_ROLE ON USER_ID = USER_ID_FK " + + " INNER JOIN LA_ROLE ON ROLE_ID_FK = ROLE_ID WHERE ROLE_NAME = :roleName ORDER BY USER_PROVIDER, USER_NAME") + List findUserIdentifierByRole(@Bind("roleName") String roleName); + + @Query("SELECT USER_ID, USER_PROVIDER, USER_NAME, USER_EMAIL, USER_DISPLAY_NAME, USER_ENABLED, USER_EMAIL_NOTIFICATION, USER_MEMBER_SINCE FROM LA_USER " + + " INNER JOIN LA_PROJECT_USER_ROLE ON USER_ID = USER_ID_FK " + + " INNER JOIN LA_PROJECT_ROLE ON PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID " + + " WHERE PROJECT_ROLE_NAME = :roleName AND LA_PROJECT_ROLE.PROJECT_ID_FK = :projectId AND " + + " LA_PROJECT_USER_ROLE.PROJECT_ID_FK = :projectId ORDER BY USER_PROVIDER, USER_NAME") + List findUserByRoleAndProjectId(@Bind("roleName") String roleName, @Bind("projectId") int projectId); + + @Query("SELECT USER_PROVIDER, USER_NAME FROM LA_USER " + + " INNER JOIN LA_PROJECT_USER_ROLE ON USER_ID = USER_ID_FK " + + " INNER JOIN LA_PROJECT_ROLE ON PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID " + + " WHERE PROJECT_ROLE_NAME = :roleName AND LA_PROJECT_ROLE.PROJECT_ID_FK = :projectId AND " + + " LA_PROJECT_USER_ROLE.PROJECT_ID_FK = :projectId ORDER BY USER_PROVIDER, USER_NAME") + List findUserIdentifierByRoleAndProjectId(@Bind("roleName") String roleName, + @Bind("projectId") int projectId); + +} diff --git a/src/main/java/io/lavagna/query/ProjectQuery.java b/src/main/java/io/lavagna/query/ProjectQuery.java new file mode 100644 index 000000000..6f72b70d7 --- /dev/null +++ b/src/main/java/io/lavagna/query/ProjectQuery.java @@ -0,0 +1,129 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.Project; +import io.lavagna.model.ProjectWithEventCounts; + +import java.util.Collection; +import java.util.List; + +@QueryRepository +public interface ProjectQuery { + + @Query("INSERT INTO LA_PROJECT(PROJECT_NAME, PROJECT_SHORT_NAME, PROJECT_DESCRIPTION) VALUES (:name, :shortName, :description)") + int createProject(@Bind("name") String name, @Bind("shortName") String shortName, + @Bind("description") String description); + + @Query("UPDATE LA_PROJECT SET PROJECT_NAME = :name, PROJECT_DESCRIPTION = :description, PROJECT_ARCHIVED = :archived WHERE PROJECT_ID = :projectId") + int updateProject(@Bind("projectId") int projectId, @Bind("name") String name, + @Bind("description") String description, @Bind("archived") boolean archived); + + @Query("SELECT * FROM LA_PROJECT WHERE PROJECT_ID = IDENTITY()") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT * FROM LA_PROJECT WHERE PROJECT_ID = LAST_INSERT_ID()"), + @QueryOverride(db = DB.PGSQL, value = "SELECT * FROM LA_PROJECT WHERE PROJECT_ID = (SELECT CURRVAL(pg_get_serial_sequence('la_project','project_id')))") }) + Project findLastCreatedProject(); + + @Query("SELECT * FROM LA_PROJECT WHERE PROJECT_ID = :projectId") + Project findById(@Bind("projectId") int projectId); + + @Query("SELECT * FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = :shortName") + Project findByShortName(@Bind("shortName") String shortName); + + @Query("SELECT * FROM LA_PROJECT ORDER BY PROJECT_SHORT_NAME") + List findAll(); + + @Query("SELECT DISTINCT PROJECT_ID, PROJECT_NAME, PROJECT_SHORT_NAME, PROJECT_DESCRIPTION, PROJECT_ARCHIVED FROM LA_PROJECT "// + + " INNER JOIN LA_PROJECT_USER_ROLE ON LA_PROJECT_USER_ROLE.PROJECT_ID_FK = PROJECT_ID "// + + " INNER JOIN LA_PROJECT_ROLE ON LA_PROJECT_ROLE.PROJECT_ID_FK = PROJECT_ID "// + + " INNER JOIN LA_PROJECT_ROLE_PERMISSION ON LA_PROJECT_ROLE_PERMISSION.PROJECT_ROLE_ID_FK = PROJECT_ROLE_ID "// + + " WHERE "// + + " USER_ID_FK = :userId AND PERMISSION = :permission") + List findAllForUser(@Bind("userId") int userId, @Bind("permission") String permission); + + @Query("SELECT BOARD_PROJECT_ID_FK FROM LA_BOARD WHERE BOARD_SHORT_NAME = :shortName") + List findRelatedProjectIdByBoardShortname(@Bind("shortName") String shortName); + + @Query("SELECT BOARD_PROJECT_ID_FK FROM LA_BOARD WHERE BOARD_ID = "// + + " (SELECT BOARD_COLUMN_BOARD_ID_FK FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID = "// + + " (SELECT CARD_BOARD_COLUMN_ID_FK FROM LA_CARD WHERE CARD_ID = :cardId))") + List findRelatedProjectIdByCardId(@Bind("cardId") int cardId); + + @Query("SELECT BOARD_PROJECT_ID_FK FROM LA_BOARD WHERE BOARD_ID = (SELECT BOARD_COLUMN_BOARD_ID_FK FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID = :columnId)") + List findRelatedProjectIdByColumnId(@Bind("columnId") int columnId); + + @Query("SELECT BOARD_PROJECT_ID_FK FROM LA_BOARD WHERE BOARD_ID = "// + + "(SELECT BOARD_COLUMN_BOARD_ID_FK FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID = "// + + "(SELECT CARD_BOARD_COLUMN_ID_FK FROM LA_CARD WHERE CARD_ID = "// + + "(SELECT CARD_DATA_CARD_ID_FK FROM LA_CARD_DATA WHERE CARD_DATA_ID = :cardDataId )))") + List findRelatedProjectIdByCardDataId(@Bind("cardDataId") int cardDataId); + + @Query("SELECT CARD_LABEL_PROJECT_ID_FK FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = :labelId") + List findRelatedProjectIdByLabelId(@Bind("labelId") int labelId); + + @Query("SELECT BOARD_COLUMN_DEFINITION_PROJECT_ID_FK FROM LA_BOARD_COLUMN_DEFINITION WHERE BOARD_COLUMN_DEFINITION_ID = :id") + List findRelatedProjectIdByColumnDefinitionId(@Bind("id") int id); + + @Query("SELECT CARD_LABEL_PROJECT_ID_FK FROM LA_CARD_LABEL WHERE CARD_LABEL_ID = (SELECT CARD_LABEL_ID_FK FROM LA_CARD_LABEL_VALUE WHERE CARD_LABEL_VALUE_ID = :labelValueId)") + List findRelatedProjectIdByLabelValueId(@Bind("labelValueId") int labelValueId); + + @Query("SELECT BOARD_PROJECT_ID_FK FROM LA_BOARD WHERE BOARD_ID = "// + + "(SELECT BOARD_COLUMN_BOARD_ID_FK FROM LA_BOARD_COLUMN WHERE BOARD_COLUMN_ID = "// + + "(SELECT CARD_BOARD_COLUMN_ID_FK FROM LA_CARD WHERE CARD_ID = "// + + "(SELECT EVENT_CARD_ID_FK FROM LA_EVENT WHERE EVENT_ID = :eventId )))") + List findRelatedProjectIdByEventId(@Bind("eventId") int eventId); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES (:projectId, :value, :color)") + String createColumnDefinition(); + + @Query("UPDATE LA_BOARD_COLUMN_DEFINITION SET BOARD_COLUMN_DEFINITION_COLOR = :color WHERE BOARD_COLUMN_DEFINITION_PROJECT_ID_FK = :projectId AND BOARD_COLUMN_DEFINITION_ID = :definitionId") + int updateColumnDefinition(@Bind("color") int color, @Bind("projectId") int projectId, + @Bind("definitionId") int definitionId); + + @Query("SELECT * FROM LA_BOARD_COLUMN_DEFINITION WHERE BOARD_COLUMN_DEFINITION_PROJECT_ID_FK = :projectId") + List findColumnDefinitionsByProjectId(@Bind("projectId") int projectId); + + @Query("SELECT COUNT(PROJECT_SHORT_NAME) FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = :shortName") + Integer existsWithShortName(@Bind("shortName") String shortName); + + @Query("SELECT LA_PROJECT.PROJECT_ID, LA_PROJECT.PROJECT_NAME, LA_PROJECT.PROJECT_SHORT_NAME, PROJECT_DESCRIPTION, PROJECT_ARCHIVED, " + + "COUNT( EVENT_ID ) AS EVENTS FROM LA_PROJECT " + + "INNER JOIN LA_CARD_FULL ON LA_CARD_FULL.PROJECT_ID = LA_PROJECT.PROJECT_ID " + + "INNER JOIN LA_EVENT ON EVENT_CARD_ID_FK = LA_CARD_FULL.CARD_ID AND EVENT_USER_ID_FK = :userId " + + "GROUP BY LA_PROJECT.PROJECT_ID, PROJECT_NAME, LA_PROJECT.PROJECT_SHORT_NAME, PROJECT_DESCRIPTION " + + "ORDER BY EVENTS DESC") + List findProjectsByUserActivity(@Bind("userId") int userId); + + @Query("SELECT LA_PROJECT.PROJECT_ID, LA_PROJECT.PROJECT_NAME, LA_PROJECT.PROJECT_SHORT_NAME, PROJECT_DESCRIPTION, PROJECT_ARCHIVED, " + + "COUNT( EVENT_ID ) AS EVENTS FROM LA_PROJECT " + + "INNER JOIN LA_CARD_FULL ON LA_CARD_FULL.PROJECT_ID = LA_PROJECT.PROJECT_ID " + + "INNER JOIN LA_EVENT ON EVENT_CARD_ID_FK = LA_CARD_FULL.CARD_ID AND EVENT_USER_ID_FK = :userId " + + "WHERE LA_CARD_FULL.PROJECT_ID IN (:projects) " + + "GROUP BY LA_PROJECT.PROJECT_ID, PROJECT_NAME, LA_PROJECT.PROJECT_SHORT_NAME, PROJECT_DESCRIPTION " + + "ORDER BY EVENTS DESC") + List findProjectsByUserActivityInProjects(@Bind("userId") int userId, + @Bind("projects") Collection projectIds); + +} diff --git a/src/main/java/io/lavagna/query/SearchQuery.java b/src/main/java/io/lavagna/query/SearchQuery.java new file mode 100644 index 000000000..e49f92d40 --- /dev/null +++ b/src/main/java/io/lavagna/query/SearchQuery.java @@ -0,0 +1,140 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; + +@QueryRepository +public interface SearchQuery { + + @Query(type = QueryType.TEMPLATE, value = "SELECT COUNT(LA_CARD.CARD_ID) FROM ") + String findFirstSelectCount(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT LA_CARD.CARD_ID FROM ") + String findFirstSelect(); + + @Query(type = QueryType.TEMPLATE, value = "LA_CARD " + + " INNER JOIN LA_BOARD_COLUMN ON LA_CARD.CARD_BOARD_COLUMN_ID_FK = LA_BOARD_COLUMN.BOARD_COLUMN_ID " + + " INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + " INNER JOIN LA_BOARD ON LA_BOARD.BOARD_ID = LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK " + + " INNER JOIN LA_PROJECT ON LA_BOARD.BOARD_PROJECT_ID_FK = LA_PROJECT.PROJECT_ID " + " INNER JOIN (") + String findFirstFrom(); + + @Query(type = QueryType.TEMPLATE, value = " ) AS CARD_R ON LA_CARD.CARD_ID = CARD_R.CARD_ID ") + String findSecond(); + + @Query(type = QueryType.TEMPLATE, value = " WHERE ") + String findThirdWhere(); + + @Query(type = QueryType.TEMPLATE, value = " LA_BOARD.BOARD_ID = ? ") + String findFourthInBoardId(); + + @Query(type = QueryType.TEMPLATE, value = " LA_BOARD.BOARD_PROJECT_ID_FK = ? ") + String findInFifthProjectId(); + + @Query(type = QueryType.TEMPLATE, value = " LA_BOARD.BOARD_PROJECT_ID_FK IN ") + String findSixthRestrictedReadAccess(); + + @Query(type = QueryType.TEMPLATE, value = " ORDER BY LA_CARD.CARD_LAST_UPDATED DESC LIMIT ? OFFSET ?") + String findSeventhOrderByAndLimit(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT LA_CARD.CARD_ID FROM LA_CARD LEFT JOIN (") + String findCardIdNotInOpen(); + + @Query(type = QueryType.TEMPLATE, value = ") TO_EXCLUDE ON LA_CARD.CARD_ID = TO_EXCLUDE.CARD_ID WHERE TO_EXCLUDE.CARD_ID IS NULL") + String findCardIdNotInClose(); + + @Query(type = QueryType.TEMPLATE, value = "(SELECT CARD_ID FROM LA_CARD WHERE CARD_SEQ_NUMBER LIKE CONCAT(?, '%')) UNION (SELECT CARD_ID FROM LA_CARD WHERE LA_TEXT_SEARCH(CARD_NAME, ?)) " + + " UNION (SELECT CARD_DATA_CARD_ID_FK CARD_ID FROM LA_CARD_DATA WHERE CARD_DATA_DELETED = FALSE AND " + + " CARD_DATA_TYPE IN ('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'DESCRIPTION') AND LA_TEXT_SEARCH_CLOB(CARD_DATA_CONTENT, ?))") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "SELECT CARD_ID FROM (SELECT CARD_ID FROM LA_CARD WHERE CARD_SEQ_NUMBER LIKE CONCAT(?, '%') UNION " + + " SELECT CARD_ID FROM (SELECT CARD_FTS_SUPPORT_CARD_ID_FK CARD_ID FROM LA_CARD_FTS_SUPPORT WHERE MATCH (CARD_FTS_SUPPORT_CARD_NAME) AGAINST (? IN BOOLEAN MODE)) AS CARD_SEQ_AND_CARD_NAME" + + " UNION " + + " SELECT CARD_DATA_CARD_ID_FK CARD_ID FROM LA_CARD_DATA " + + " INNER JOIN LA_CARD_DATA_FTS_SUPPORT ON " + + " CARD_DATA_FTS_SUPPORT_CARD_DATA_ID_FK = CARD_DATA_ID " + + " WHERE CARD_DATA_DELETED <> TRUE AND " + + " CARD_DATA_TYPE IN ('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'DESCRIPTION') AND " + + " MATCH(CARD_DATA_FTS_SUPPORT_CARD_DATA_CONTENT) AGAINST (? IN BOOLEAN MODE)) AS FTS_RES"), + @QueryOverride(db = DB.PGSQL, value = "(SELECT CARD_ID FROM LA_CARD WHERE CAST(CARD_SEQ_NUMBER AS TEXT) LIKE CONCAT(?, '%')) UNION " + + " SELECT CARD_ID FROM LA_CARD WHERE card_name_tsvector @@ plainto_tsquery('english', unaccent(?)) UNION " + + " SELECT CARD_DATA_CARD_ID_FK CARD_ID FROM LA_CARD_DATA WHERE CARD_DATA_DELETED <> TRUE AND " + + " CARD_DATA_TYPE IN ('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'DESCRIPTION') AND " + + " card_data_content_tsvector @@ plainto_tsquery('english', unaccent(?))") }) + String findByFreeText(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CARD_ID FROM LA_CARD INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + " INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + " WHERE BOARD_COLUMN_DEFINITION_VALUE = ?") + String findByStatus(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CARD_ID FROM LA_CARD INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + " INNER JOIN LA_BOARD ON LA_BOARD.BOARD_ID = LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK " + + " WHERE BOARD_ARCHIVED = ?") + String findByBoardStatus(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CARD_ID FROM LA_CARD INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + " INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + " WHERE BOARD_COLUMN_LOCATION = ?") + String findByLocation(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CARD_ID_FK AS CARD_ID FROM LA_CARD_LABEL_VALUE " + + " INNER JOIN LA_CARD_LABEL ON CARD_LABEL_ID = CARD_LABEL_ID_FK " + + " WHERE CARD_LABEL_VALUE_DELETED <> TRUE AND CARD_LABEL_DOMAIN = 'SYSTEM' AND CARD_LABEL_NAME = ?") + String findBySystemLabel(); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CARD_ID_FK AS CARD_ID FROM LA_CARD_LABEL_VALUE " + + " INNER JOIN LA_CARD_LABEL ON CARD_LABEL_ID = CARD_LABEL_ID_FK " + + " WHERE CARD_LABEL_VALUE_DELETED <> TRUE AND CARD_LABEL_DOMAIN = 'USER' AND CARD_LABEL_NAME LIKE CONCAT(? ,'%')") + String findByUserLabel(); + + @Query(type = QueryType.TEMPLATE, value = " AND (CASE " + + " WHEN CARD_LABEL_VALUE_TYPE = 'STRING' THEN CARD_LABEL_VALUE_STRING LIKE CONCAT(?, '%') " + + " WHEN CARD_LABEL_VALUE_TYPE = 'INT' THEN CARD_LABEL_VALUE_INT = ? " + + " WHEN CARD_LABEL_VALUE_TYPE = 'TIMESTAMP' THEN CARD_LABEL_VALUE_TIMESTAMP BETWEEN ? AND ? " + + " WHEN CARD_LABEL_VALUE_TYPE = 'USER' THEN CARD_LABEL_VALUE_USER_FK = ? " + + " WHEN CARD_LABEL_VALUE_TYPE = 'CARD' THEN CARD_LABEL_VALUE_CARD_FK = ? " + + " WHEN CARD_LABEL_VALUE_TYPE = 'LIST' THEN (CARD_LABEL_ID_FK, CARD_LABEL_VALUE_LIST_VALUE_FK) IN (SELECT CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = ?) " + + " END)") + String andLabelValueString(); + + @Query(type = QueryType.TEMPLATE, value = " AND CARD_LABEL_VALUE_TIMESTAMP >= ? AND CARD_LABEL_VALUE_TIMESTAMP < ?") + String andLabelValueDate(); + + @Query(type = QueryType.TEMPLATE, value = " AND CARD_LABEL_VALUE_USER_FK = ? ") + String andLabelValueUser(); + + @Query(type = QueryType.TEMPLATE, value = " AND (CARD_LABEL_ID_FK, CARD_LABEL_VALUE_LIST_VALUE_FK) IN (SELECT CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = ?) ") + String andLabelListValueEq(); + + @Query(type = QueryType.TEMPLATE, value = " SELECT CARD_ID FROM LA_CARD INNER JOIN LA_EVENT ON CARD_ID = EVENT_CARD_ID_FK WHERE EVENT_TYPE = 'CARD_CREATE' AND EVENT_TIME BETWEEN ? AND ? ") + String findByCardCreationEventDate(); + + @Query(type = QueryType.TEMPLATE, value = " SELECT CARD_ID FROM LA_CARD INNER JOIN LA_EVENT ON CARD_ID = EVENT_CARD_ID_FK WHERE EVENT_TYPE = 'CARD_CREATE' AND EVENT_USER_ID_FK = ? ") + String findByCardCreationEventUser(); + + @Query(type = QueryType.TEMPLATE, value = " SELECT CARD_ID FROM LA_CARD WHERE CARD_LAST_UPDATED BETWEEN ? AND ? ") + String findByUpdated(); + + @Query(type = QueryType.TEMPLATE, value = " SELECT CARD_ID FROM LA_CARD WHERE CARD_LAST_UPDATED_USER_ID_FK = ? ") + String findByUpdatedBy(); +} diff --git a/src/main/java/io/lavagna/query/StatisticsQuery.java b/src/main/java/io/lavagna/query/StatisticsQuery.java new file mode 100644 index 000000000..cb08f84e2 --- /dev/null +++ b/src/main/java/io/lavagna/query/StatisticsQuery.java @@ -0,0 +1,243 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.model.CardFull; +import io.lavagna.model.EventsCount; +import io.lavagna.model.LabelAndValueWithCount; +import io.lavagna.model.MilestoneCount; +import io.lavagna.model.StatisticForExport; +import io.lavagna.model.StatisticsResult; + +import java.util.Date; +import java.util.List; + +@QueryRepository +public interface StatisticsQuery { + + @Query("INSERT INTO LA_BOARD_STATISTICS SELECT :date, BOARD_ID, BOARD_COLUMN_DEFINITION_ID_FK, BOARD_COLUMN_LOCATION, COUNT(*) AS CARDS_COUNT FROM LA_CARD " + + "INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK " + + "INNER JOIN LA_BOARD ON LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "GROUP BY BOARD_ID, BOARD_COLUMN_DEFINITION_ID_FK, BOARD_COLUMN_LOCATION") + void snapshotCardsStatus(@Bind("date") Date date); + + @Query("DELETE FROM LA_BOARD_STATISTICS WHERE BOARD_STATISTICS_TIME NOT IN (SELECT DAY FROM LA_BOARD_STATISTICS_DAYS)") + @QueriesOverride({ + @QueryOverride(db = DB.MYSQL, value = "DELETE BOARD_STATS FROM LA_BOARD_STATISTICS AS BOARD_STATS LEFT JOIN LA_BOARD_STATISTICS_DAYS ON BOARD_STATS.BOARD_STATISTICS_TIME = LA_BOARD_STATISTICS_DAYS.DAY WHERE LA_BOARD_STATISTICS_DAYS.DAY IS NULL")}) + void cleanOldCardsStatusSnapshots(); + + + @Query("SELECT BOARD_STATISTICS_TIME AS TIME, BOARD_COLUMN_DEFINITION_VALUE, SUM(BOARD_STATISTICS_COUNT) AS STATISTICS_COUNT FROM LA_BOARD_STATISTICS " + + "INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + "INNER JOIN LA_BOARD_STATISTICS_DAYS ON BOARD_STATISTICS_TIME = DAY " + + "INNER JOIN LA_BOARD ON BOARD_STATISTICS_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "WHERE BOARD_STATISTICS_LOCATION = 'BOARD' AND BOARD_STATISTICS_BOARD_ID_FK = :boardId AND BOARD_STATISTICS_TIME >= :fromDate " + + "GROUP BY BOARD_STATISTICS_TIME, BOARD_COLUMN_DEFINITION_VALUE") + List getCardsStatusByBoard(@Bind("boardId") int boardId, @Bind("fromDate") Date fromDate); + + @Query("SELECT BOARD_STATISTICS_TIME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_STATISTICS_LOCATION, BOARD_STATISTICS_COUNT FROM LA_BOARD_STATISTICS " + + "INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID WHERE BOARD_STATISTICS_BOARD_ID_FK = :boardId") + List findForBoard(@Bind("boardId") int boardId); + + @Query("INSERT INTO LA_BOARD_STATISTICS(BOARD_STATISTICS_TIME, BOARD_STATISTICS_BOARD_ID_FK, BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK, BOARD_STATISTICS_LOCATION, BOARD_STATISTICS_COUNT) " + + " VALUES(:date, :boardId, :boardColumnDefinitionId, :location, :count)") + int addFromImport(@Bind("date") Date date, @Bind("boardId") int boardId, + @Bind("boardColumnDefinitionId") int boardColumnDefinitionId, @Bind("location") String location, + @Bind("count") long count); + + @Query("SELECT BOARD_STATISTICS_TIME AS TIME, BOARD_COLUMN_DEFINITION_VALUE, SUM(BOARD_STATISTICS_COUNT) AS STATISTICS_COUNT FROM LA_BOARD_STATISTICS " + + "INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + "INNER JOIN LA_BOARD_STATISTICS_DAYS ON BOARD_STATISTICS_TIME = DAY " + + "INNER JOIN LA_BOARD ON BOARD_STATISTICS_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "WHERE BOARD_STATISTICS_LOCATION = 'BOARD' AND BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND BOARD_STATISTICS_TIME >= :fromDate " + + "GROUP BY BOARD_STATISTICS_TIME, BOARD_COLUMN_DEFINITION_VALUE") + List getCardsStatusByProject(@Bind("projectId") int projectId, @Bind("fromDate") Date fromDate); + + @Query("SELECT COUNT(*) FROM (SELECT DISTINCT EVENT_USER_ID_FK FROM LA_PROJECT " + + "INNER JOIN LA_CARD_FULL ON LA_CARD_FULL.PROJECT_SHORT_NAME = LA_PROJECT.PROJECT_SHORT_NAME " + + "INNER JOIN LA_BOARD ON LA_CARD_FULL.BOARD_SHORT_NAME = LA_BOARD.BOARD_SHORT_NAME " + + "INNER JOIN LA_EVENT ON EVENT_CARD_ID_FK = LA_CARD_FULL.CARD_ID " + + "WHERE BOARD_ID = :boardId AND EVENT_TIME >= :fromDate) AS USERS") + Integer getActiveUsersOnBoard(@Bind("boardId") int boardId, @Bind("fromDate") Date fromDate); + + // Average users per card + + @Query("SELECT COUNT(*) FROM (SELECT DISTINCT EVENT_USER_ID_FK FROM LA_PROJECT " + + "INNER JOIN LA_CARD_FULL ON LA_CARD_FULL.PROJECT_SHORT_NAME = LA_PROJECT.PROJECT_SHORT_NAME " + + "INNER JOIN LA_BOARD ON LA_CARD_FULL.BOARD_SHORT_NAME = LA_BOARD.BOARD_SHORT_NAME " + + "INNER JOIN LA_EVENT ON EVENT_CARD_ID_FK = LA_CARD_FULL.CARD_ID " + + "WHERE BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND EVENT_TIME >= :fromDate) AS USERS") + Integer getActiveUsersOnProject(@Bind("projectId") int projectId, @Bind("fromDate") Date fromDate); + + @Query("SELECT AVG(USERS) FROM ( " + "SELECT CARD_ID, COUNT(ASSIGNED_USER_ID) AS USERS " + "FROM LA_CARD_FULL " + + "LEFT JOIN LA_ASSIGNED_CARD ON LA_CARD_FULL.CARD_ID = LA_ASSIGNED_CARD.ASSIGNED_CARD_ID " + + "INNER JOIN LA_BOARD ON LA_CARD_FULL.BOARD_SHORT_NAME = LA_BOARD.BOARD_SHORT_NAME " + + "WHERE BOARD_ID = :boardId AND BOARD_COLUMN_LOCATION = 'BOARD' " + "GROUP BY CARD_ID) AS ASSIGNED ") + Double getAverageUsersPerCardOnBoard(@Bind("boardId") int boardId); + + @Query("SELECT AVG(USERS) FROM ( " + "SELECT CARD_ID, COUNT(ASSIGNED_USER_ID) AS USERS " + "FROM LA_CARD_FULL " + + "LEFT JOIN LA_ASSIGNED_CARD ON LA_CARD_FULL.CARD_ID = LA_ASSIGNED_CARD.ASSIGNED_CARD_ID " + + "INNER JOIN LA_BOARD ON LA_CARD_FULL.BOARD_SHORT_NAME = LA_BOARD.BOARD_SHORT_NAME " + + "WHERE BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY CARD_ID) AS ASSIGNED ") + Double getAverageUsersPerCardOnProject(@Bind("projectId") int projectId); + + // Average cards per user + + @Query("SELECT AVG(CARDS) FROM ( " + "SELECT COUNT(ASSIGNED_CARD_ID) AS CARDS, ASSIGNED_USER_ID " + + "FROM LA_ASSIGNED_CARD " + + "INNER JOIN LA_CARD_FULL ON LA_CARD_FULL.CARD_ID = LA_ASSIGNED_CARD.ASSIGNED_CARD_ID " + + "INNER JOIN LA_BOARD ON LA_CARD_FULL.BOARD_SHORT_NAME = LA_BOARD.BOARD_SHORT_NAME " + + "WHERE BOARD_ID = :boardId AND BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY ASSIGNED_USER_ID) AS ASSIGNED ") + Double getAverageCardsPerUserOnBoard(@Bind("boardId") int boardId); + + @Query("SELECT AVG(CARDS) FROM ( " + "SELECT COUNT(ASSIGNED_CARD_ID) AS CARDS, ASSIGNED_USER_ID " + + "FROM LA_ASSIGNED_CARD " + + "INNER JOIN LA_CARD_FULL ON LA_CARD_FULL.CARD_ID = LA_ASSIGNED_CARD.ASSIGNED_CARD_ID " + + "INNER JOIN LA_BOARD ON LA_CARD_FULL.BOARD_SHORT_NAME = LA_BOARD.BOARD_SHORT_NAME " + + "WHERE BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY ASSIGNED_USER_ID) AS ASSIGNED ") + Double getAverageCardsPerUserOnProject(@Bind("projectId") int projectId); + + // Cards by label + + @Query("SELECT CARD_LABEL_ID, CARD_LABEL_NAME, CARD_LABEL_COLOR, " + + "CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK, COUNT(*) AS LABEL_COUNT FROM LA_CARD " + + "INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID " + + "INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK " + + "INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK " + + "INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'USER' AND BOARD_ID = :boardId " + + "AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE AND BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY CARD_LABEL_ID, CARD_LABEL_NAME, CARD_LABEL_COLOR, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK " + + "ORDER BY CARD_LABEL_NAME") + List getCardsByLabelOnBoard(@Bind("boardId") int boardId); + + @Query("SELECT CARD_LABEL_ID, CARD_LABEL_NAME, CARD_LABEL_COLOR, " + + "CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK, COUNT(*) AS LABEL_COUNT FROM LA_CARD " + + "INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID " + + "INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK " + + "INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK " + + "INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'USER' AND BOARD_PROJECT_ID_FK = :projectId " + + "AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE AND BOARD_ARCHIVED = FALSE AND BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY CARD_LABEL_ID, CARD_LABEL_NAME, CARD_LABEL_COLOR, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK " + + "ORDER BY CARD_LABEL_NAME") + List getCardsByLabelOnProject(@Bind("projectId") int projectId); + + // Created cards by date + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + "INNER JOIN LA_BOARD_COLUMN ON EVENT_COLUMN_ID_FK = BOARD_COLUMN_ID " + + "INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID " + + "WHERE EVENT_TYPE = 'CARD_CREATE' AND BOARD_ID = :boardId AND EVENT_TIME >= :fromDate " + + "GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getCreatedCardsByBoard(@Bind("boardId") int boardId, @Bind("fromDate") Date fromDate); + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + "INNER JOIN LA_BOARD_COLUMN ON EVENT_COLUMN_ID_FK = BOARD_COLUMN_ID " + + "INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID " + + "WHERE EVENT_TYPE = 'CARD_CREATE' AND BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND EVENT_TIME >= :fromDate " + + "GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getCreatedCardsByProject(@Bind("projectId") int projectId, @Bind("fromDate") Date fromDate); + + // Closed cards by date + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + "JOIN LA_BOARD_COLUMN old on LA_EVENT.EVENT_PREV_COLUMN_ID_FK = old.BOARD_COLUMN_ID " + + "JOIN LA_BOARD_COLUMN_DEFINITION oldDef on old.BOARD_COLUMN_DEFINITION_ID_FK = oldDef.BOARD_COLUMN_DEFINITION_ID " + + "JOIN LA_BOARD_COLUMN new on LA_EVENT.EVENT_COLUMN_ID_FK = new.BOARD_COLUMN_ID " + + "JOIN LA_BOARD_COLUMN_DEFINITION newDef on new.BOARD_COLUMN_DEFINITION_ID_FK = newDef.BOARD_COLUMN_DEFINITION_ID " + + "JOIN LA_BOARD ON new.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "WHERE (EVENT_TYPE = 'CARD_MOVE' OR EVENT_TYPE = 'CARD_ARCHIVE' OR EVENT_TYPE = 'CARD_TRASH') AND " + + "BOARD_ID = :boardId AND EVENT_TIME >= :fromDate AND " + + "oldDef.BOARD_COLUMN_DEFINITION_VALUE <> 'CLOSED' AND newDef.BOARD_COLUMN_DEFINITION_VALUE = 'CLOSED' " + + "GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getClosedCardsByBoard(@Bind("boardId") int boardId, @Bind("fromDate") Date fromDate); + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + "JOIN LA_BOARD_COLUMN old on LA_EVENT.EVENT_PREV_COLUMN_ID_FK = old.BOARD_COLUMN_ID " + + "JOIN LA_BOARD_COLUMN_DEFINITION oldDef on old.BOARD_COLUMN_DEFINITION_ID_FK = oldDef.BOARD_COLUMN_DEFINITION_ID " + + "JOIN LA_BOARD_COLUMN new on LA_EVENT.EVENT_COLUMN_ID_FK = new.BOARD_COLUMN_ID " + + "JOIN LA_BOARD_COLUMN_DEFINITION newDef on new.BOARD_COLUMN_DEFINITION_ID_FK = newDef.BOARD_COLUMN_DEFINITION_ID " + + "JOIN LA_BOARD ON new.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID " + + "WHERE (EVENT_TYPE = 'CARD_MOVE' OR EVENT_TYPE = 'CARD_ARCHIVE' OR EVENT_TYPE = 'CARD_TRASH') AND " + + "BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND EVENT_TIME >= :fromDate AND " + + "oldDef.BOARD_COLUMN_DEFINITION_VALUE <> 'CLOSED' AND newDef.BOARD_COLUMN_DEFINITION_VALUE = 'CLOSED' " + + "GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getClosedCardsByProject(@Bind("projectId") int projectId, @Bind("fromDate") Date fromDate); + + // Most active card + + @Query("SELECT CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, LA_CARD_FULL.BOARD_SHORT_NAME, LA_CARD_FULL.PROJECT_SHORT_NAME, COUNT(*) AS EVENTS_COUNT FROM LA_EVENT " + + "INNER JOIN LA_CARD_FULL ON CARD_ID = EVENT_CARD_ID_FK " + + "INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + "INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID " + + "WHERE BOARD_ID = :boardId AND EVENT_TIME >= :fromDate AND LA_BOARD_COLUMN.BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, LA_CARD_FULL.BOARD_SHORT_NAME, PROJECT_SHORT_NAME ORDER BY EVENTS_COUNT DESC LIMIT 1") + CardFull getMostActiveCardByBoard(@Bind("boardId") int boardId, @Bind("fromDate") Date fromDate); + + @Query("SELECT CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, LA_CARD_FULL.BOARD_SHORT_NAME, LA_CARD_FULL.PROJECT_SHORT_NAME, COUNT(*) AS EVENTS_COUNT FROM LA_EVENT " + + "INNER JOIN LA_CARD_FULL ON CARD_ID = EVENT_CARD_ID_FK " + + "INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID " + + "INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID " + + "WHERE BOARD_ARCHIVED = FALSE AND BOARD_PROJECT_ID_FK = :projectId AND EVENT_TIME >= :fromDate AND LA_BOARD_COLUMN.BOARD_COLUMN_LOCATION = 'BOARD' " + + "GROUP BY CARD_ID, CARD_NAME, CARD_SEQ_NUMBER, CARD_ORDER, CARD_BOARD_COLUMN_ID_FK, CREATE_USER, CREATE_TIME, LAST_UPDATE_USER, LAST_UPDATE_TIME, BOARD_COLUMN_DEFINITION_VALUE, LA_CARD_FULL.BOARD_SHORT_NAME, PROJECT_SHORT_NAME ORDER BY EVENTS_COUNT DESC LIMIT 1") + CardFull getMostActiveCardByProject(@Bind("projectId") int projectId, @Bind("fromDate") Date fromDate); + + // Milestones + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + "WHERE EVENT_TYPE = 'LABEL_CREATE' AND EVENT_VALUE_STRING = :milestone AND EVENT_LABEL_NAME = 'MILESTONE' AND EVENT_TIME >= :fromDate " + + "GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getAssignedCardsByMilestone(@Bind("milestone") String milestone, @Bind("fromDate") Date fromDate); + + @Query("SELECT CAST(EVENT_TIME AS DATE) AS EVENT_DATE, COUNT(*) AS EVENT_COUNT FROM LA_EVENT " + + "JOIN LA_BOARD_COLUMN old on LA_EVENT.EVENT_PREV_COLUMN_ID_FK = old.BOARD_COLUMN_ID " + + "JOIN LA_BOARD_COLUMN_DEFINITION oldDef on old.BOARD_COLUMN_DEFINITION_ID_FK = oldDef.BOARD_COLUMN_DEFINITION_ID " + + "JOIN LA_BOARD_COLUMN new on LA_EVENT.EVENT_COLUMN_ID_FK = new.BOARD_COLUMN_ID " + + "JOIN LA_BOARD_COLUMN_DEFINITION newDef on new.BOARD_COLUMN_DEFINITION_ID_FK = newDef.BOARD_COLUMN_DEFINITION_ID " + + "INNER JOIN LA_CARD_LABEL_VALUE ON EVENT_CARD_ID_FK = CARD_ID_FK AND CARD_LABEL_VALUE_DELETED <> TRUE " + + "WHERE (EVENT_TYPE = 'CARD_MOVE' OR EVENT_TYPE = 'CARD_ARCHIVE' OR EVENT_TYPE = 'CARD_TRASH') AND " + + "CARD_LABEL_VALUE_LIST_VALUE_FK = :milestoneId AND EVENT_TIME >= :fromDate AND " + + "oldDef.BOARD_COLUMN_DEFINITION_VALUE <> 'CLOSED' AND newDef.BOARD_COLUMN_DEFINITION_VALUE = 'CLOSED' " + + "GROUP BY EVENT_DATE ORDER BY EVENT_DATE") + List getClosedCardsByMilestone(@Bind("milestoneId") int milestoneId, @Bind("fromDate") Date fromDate); + + @Query("(SELECT CARD_LABEL_VALUE_LIST_VALUE_FK, BOARD_COLUMN_DEFINITION_VALUE, COUNT(BOARD_COLUMN_DEFINITION_VALUE) AS MILESTONE_COUNT FROM LA_CARD " + + "INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID AND BOARD_COLUMN_LOCATION <> 'TRASH' " + + "INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + "INNER JOIN LA_BOARD ON BOARD_ID = BOARD_COLUMN_BOARD_ID_FK AND BOARD_PROJECT_ID_FK = :projectId " + + "INNER JOIN LA_CARD_LABEL_VALUE ON CARD_ID = CARD_ID_FK AND CARD_LABEL_VALUE_DELETED <> TRUE " + + "INNER JOIN LA_CARD_LABEL ON CARD_LABEL_ID = CARD_LABEL_ID_FK AND CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_DOMAIN = 'SYSTEM' " + + "GROUP BY CARD_LABEL_VALUE_LIST_VALUE_FK, BOARD_COLUMN_DEFINITION_VALUE) " + + "UNION (SELECT NULL, BOARD_COLUMN_DEFINITION_VALUE, COUNT(BOARD_COLUMN_DEFINITION_VALUE) FROM LA_CARD " + + "INNER JOIN LA_BOARD_COLUMN ON CARD_BOARD_COLUMN_ID_FK = BOARD_COLUMN_ID AND BOARD_COLUMN_LOCATION <> 'TRASH' " + + "INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID " + + "INNER JOIN LA_BOARD ON BOARD_ID = BOARD_COLUMN_BOARD_ID_FK AND BOARD_PROJECT_ID_FK = :projectId " + + "WHERE CARD_ID NOT IN (SELECT CARD_ID_FK AS CARD_ID FROM LA_CARD_LABEL_VALUE " + + "INNER JOIN LA_CARD_LABEL ON CARD_LABEL_ID = CARD_LABEL_ID_FK " + + "WHERE CARD_LABEL_VALUE_DELETED <> TRUE AND CARD_LABEL_DOMAIN = 'SYSTEM' AND CARD_LABEL_NAME = 'MILESTONE') " + + "GROUP BY BOARD_COLUMN_DEFINITION_VALUE) ") + List findCardsCountByMilestone(@Bind("projectId") int projectId); +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/query/UserQuery.java b/src/main/java/io/lavagna/query/UserQuery.java new file mode 100644 index 000000000..eda3c1a24 --- /dev/null +++ b/src/main/java/io/lavagna/query/UserQuery.java @@ -0,0 +1,103 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.Bind; +import io.lavagna.common.Query; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; +import io.lavagna.model.User; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +@QueryRepository +public interface UserQuery { + + @Query("INSERT INTO LA_USER(USER_PROVIDER, USER_NAME, USER_EMAIL, USER_DISPLAY_NAME, USER_ENABLED) VALUES (:provider, :userName, :email, :displayName, :enabled)") + int createUser(@Bind("provider") String provider, @Bind("userName") String username, @Bind("email") String email, + @Bind("displayName") String displayName, @Bind("enabled") boolean enabled); + + @Query(type = QueryType.TEMPLATE, value = "INSERT INTO LA_USER(USER_PROVIDER, USER_NAME, USER_EMAIL, USER_DISPLAY_NAME, USER_ENABLED, USER_EMAIL_NOTIFICATION, USER_MEMBER_SINCE) VALUES " + + " (:provider, :userName, :email, :displayName, :enabled, :emailNotification, :memberSince)") + String createUserFull(); + + @Query("SELECT * FROM LA_USER WHERE USER_NAME = :userName AND USER_PROVIDER = :provider") + User findUserByName(@Bind("provider") String provider, @Bind("userName") String userName); + + @Query("SELECT * FROM LA_USER WHERE USER_ID = :userId") + User findUserById(@Bind("userId") int userId); + + @Query("SELECT * FROM LA_USER WHERE USER_ID IN (:userIds)") + List findByIds(@Bind("userIds") Collection userIds); + + @Query("SELECT COUNT(*) FROM LA_USER WHERE USER_NAME = :userName AND USER_PROVIDER = :provider AND (USER_ENABLED IS NULL OR USER_ENABLED = :enabled) ") + Integer userExistsAndEnabled(@Bind("provider") String provider, @Bind("userName") String userName, + @Bind("enabled") boolean enabled); + + @Query("SELECT COUNT(*) FROM LA_USER WHERE USER_NAME = :userName AND USER_PROVIDER = :provider") + Integer userExistsAndEnabled(@Bind("provider") String provider, @Bind("userName") String userName); + + String FIND_USER_COMMON_WHERE = " WHERE LOWER(USER_PROVIDER) <> 'system' AND " + + "(LOWER(USER_PROVIDER) LIKE CONCAT('%', LOWER(:criteria),'%') OR LOWER(USER_NAME) LIKE CONCAT('%', LOWER(:criteria),'%') OR LOWER(USER_EMAIL) LIKE CONCAT('%', LOWER(:criteria),'%') " + + " OR LOWER(USER_DISPLAY_NAME) LIKE CONCAT('%', LOWER(:criteria),'%') ) ORDER BY USER_PROVIDER, USER_NAME LIMIT 10"; + + @Query("SELECT * FROM LA_USER " + FIND_USER_COMMON_WHERE) + List findUsers(@Bind("criteria") String criteria); + + @Query("SELECT * FROM LA_USER "// + + "inner join "// + + "(select USER_ID_FK from LA_PROJECT_ROLE_PERMISSION "// + + "inner join LA_PROJECT_ROLE on LA_PROJECT_ROLE_PERMISSION.project_role_id_fk = project_role_id "// + + "inner join LA_PROJECT_USER_ROLE on LA_PROJECT_USER_ROLE.project_role_id_fk = project_role_id "// + + "WHERE PERMISSION = :permission AND LA_PROJECT_ROLE.PROJECT_ID_FK = :projectId "// + + "union "// + + "select USER_ID_FK from LA_ROLE_PERMISSION "// + + "inner join LA_ROLE on LA_ROLE_PERMISSION.role_id_fk = role_id "// + + "inner join LA_USER_ROLE on LA_USER_ROLE .role_id_fk = role_id "// + + "WHERE PERMISSION = :permission) as filtered_users on user_id = user_id_fk "// + + FIND_USER_COMMON_WHERE) + List findUsers(@Bind("criteria") String criteria, @Bind("projectId") int projectId, + @Bind("permission") String permission); + + @Query("UPDATE LA_USER SET USER_EMAIL = :email, USER_DISPLAY_NAME = :displayName, USER_EMAIL_NOTIFICATION = :emailNotification WHERE USER_ID = :userId") + int updateProfile(@Bind("email") String email, @Bind("displayName") String displayName, + @Bind("emailNotification") boolean emailNotification, @Bind("userId") int userId); + + @Query("SELECT * FROM LA_USER ORDER BY USER_PROVIDER, USER_NAME") + List findAll(); + + @Query("UPDATE LA_USER SET USER_ENABLED = :enabled WHERE USER_ID = :userId") + int toggle(@Bind("enabled") boolean enabled, @Bind("userId") int userId); + + @Query(type = QueryType.TEMPLATE, value = "SELECT CONCAT(CONCAT(USER_PROVIDER, ':'), USER_NAME) AS PROVIDER_USER, USER_ID FROM LA_USER WHERE (USER_PROVIDER, USER_NAME) IN (:users)") + String findUsersId(); + + @Query("INSERT INTO LA_USER_REMEMBER(USER_REMEMBER_HASHED_TOKEN, USER_REMEMBER_ID_FK, USER_REMEMBER_LAST_USE) VALUES (:hashedToken, :userId, :lastUse)") + int registerRememberMeToken(@Bind("hashedToken") String hashedToken, @Bind("userId") int userId, + @Bind("lastUse") Date lastUse); + + @Query("DELETE FROM LA_USER_REMEMBER WHERE USER_REMEMBER_HASHED_TOKEN = :hashedToken AND USER_REMEMBER_ID_FK = :userId") + int deleteToken(@Bind("hashedToken") String hashedToken, @Bind("userId") int userId); + + @Query("SELECT COUNT(*) FROM LA_USER_REMEMBER WHERE USER_REMEMBER_HASHED_TOKEN = :hashedToken AND USER_REMEMBER_ID_FK = :userId") + Integer tokenExists(@Bind("hashedToken") String hashedToken, @Bind("userId") int userId); + + @Query("DELETE FROM LA_USER_REMEMBER WHERE USER_REMEMBER_ID_FK = :userId") + int deleteAllTokensForUserId(@Bind("userId") int id); +} diff --git a/src/main/java/io/lavagna/query/ValidationQuery.java b/src/main/java/io/lavagna/query/ValidationQuery.java new file mode 100644 index 000000000..a70180cb9 --- /dev/null +++ b/src/main/java/io/lavagna/query/ValidationQuery.java @@ -0,0 +1,32 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.query; + +import io.lavagna.common.QueriesOverride; +import io.lavagna.common.Query; +import io.lavagna.common.QueryOverride; +import io.lavagna.common.QueryRepository; +import io.lavagna.common.QueryType; + +@QueryRepository +public interface ValidationQuery { + + @Query(type = QueryType.TEMPLATE, value = "SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS") + @QueriesOverride({ @QueryOverride(db = DB.MYSQL, value = "SELECT 1"), + @QueryOverride(db = DB.PGSQL, value = "SELECT 1") }) + String validation(); +} diff --git a/src/main/java/io/lavagna/service/BoardColumnRepository.java b/src/main/java/io/lavagna/service/BoardColumnRepository.java new file mode 100644 index 000000000..607944841 --- /dev/null +++ b/src/main/java/io/lavagna/service/BoardColumnRepository.java @@ -0,0 +1,156 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.StringUtils.trimToNull; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.BoardColumnInfo; +import io.lavagna.model.User; +import io.lavagna.query.BoardColumnQuery; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional(readOnly = true) +public class BoardColumnRepository { + + private final NamedParameterJdbcTemplate jdbc; + private final EventRepository eventRepository; + private final BoardColumnQuery queries; + + @Autowired + public BoardColumnRepository(NamedParameterJdbcTemplate jdbc, EventRepository eventRepository, + BoardColumnQuery queries) { + this.jdbc = jdbc; + this.eventRepository = eventRepository; + this.queries = queries; + } + + public BoardColumnInfo getColumnInfoById(int columnId) { + return queries.getColumnInfoById(columnId); + } + + public BoardColumn findById(int columnId) { + return queries.findById(columnId); + } + + public BoardColumn findDefaultColumnFor(int boardId, BoardColumnLocation location) { + return queries.findDefaultColumnFor(boardId, location.toString()); + } + + public List findAllColumnsFor(int boardId) { + return queries.findAllColumnFor(boardId); + } + + public int updateOrder(int columnId, int order) { + return queries.updateOrder(columnId, order); + } + + public List findAllColumnsFor(int boardId, BoardColumnLocation location) { + return queries.findAllColumnFor(boardId, location.toString()); + } + + public List findByIds(Set ids) { + if (ids.isEmpty()) { + return Collections.emptyList(); + } + return queries.findByIds(ids); + } + + /** + * Returns the new Column + * + * @param name + * @param boardId + * @return + */ + @Transactional(readOnly = false) + public BoardColumn addColumnToBoard(String name, int definitionId, BoardColumnLocation location, int boardId) { + Objects.requireNonNull(name); + Objects.requireNonNull(location); + + queries.addColumnToBoard(trimToNull(name), boardId, location.toString(), definitionId); + + return queries.findLastCreatedColumn(); + } + + @Transactional(readOnly = false) + public int renameColumn(int columnId, String newName, int boardId) { + return queries.renameColumn(trimToNull(newName), columnId, boardId); + } + + /** + * update column order in a given board/location. The column ids are filtered. + * + * @param columns + * @param boardId + * @param location + */ + @Transactional(readOnly = false) + public void updateColumnOrder(List columns, int boardId, BoardColumnLocation location) { + Objects.requireNonNull(columns); + Objects.requireNonNull(location); + + // keep only the columns that are inside the boardId and have location=board + List filteredColumns = Utils.filter(columns, + queries.findColumnIdsInBoard(columns, location.toString(), boardId)); + // + + SqlParameterSource[] params = new SqlParameterSource[filteredColumns.size()]; + for (int i = 0; i < filteredColumns.size(); i++) { + params[i] = new MapSqlParameterSource("order", i + 1).addValue("columnId", filteredColumns.get(i)) + .addValue("boardId", boardId).addValue("location", location.toString()); + } + + jdbc.batchUpdate(queries.updateColumnOrder(), params); + } + + @Transactional(readOnly = false) + public int moveToLocation(int id, BoardColumnLocation location, User user) { + Validate.isTrue(location != BoardColumnLocation.BOARD); + + // copy the column definition id of the default one + int columnDefinitionId = findDefaultColumnFor(findById(id).getBoardId(), location).getDefinitionId(); + // + + int res = queries.moveToLocation(id, location.toString(), columnDefinitionId); + + List cardIds = queries.findCardsInColumnId(id); + eventRepository.insertCardEvent(cardIds, id, user.getId(), BoardColumnLocation.MAPPING.get(location), + new Date()); + + return res; + } + + @Transactional(readOnly = false) + public int redefineColumn(int columnId, int definitionId, int boardId) { + return queries.redefineColumn(definitionId, columnId, boardId); + } +} diff --git a/src/main/java/io/lavagna/service/BoardRepository.java b/src/main/java/io/lavagna/service/BoardRepository.java new file mode 100644 index 000000000..15f558910 --- /dev/null +++ b/src/main/java/io/lavagna/service/BoardRepository.java @@ -0,0 +1,122 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.StringUtils.trimToNull; +import io.lavagna.model.Board; +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.BoardInfo; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.ProjectAndBoard; +import io.lavagna.query.BoardQuery; + +import java.util.List; +import java.util.Locale; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional(readOnly = true) +public class BoardRepository { + + private final BoardColumnRepository boardColumnRepository; + private final BoardQuery queries; + + @Autowired + public BoardRepository(BoardQuery queries, BoardColumnRepository boardColumnRepository) { + this.boardColumnRepository = boardColumnRepository; + this.queries = queries; + } + + @Transactional(readOnly = false) + public Board createEmptyBoard(String name, String shortName, String description, int projectId) { + queries.createNewBoard(trimToNull(name), trimToNull(shortName.toUpperCase(Locale.ENGLISH)), + trimToNull(description), projectId); + queries.initializeSequence(); + + return queries.findLastCreatedBoard(); + } + + /** + * Returns the new BOARD. + *

+ * Additionally, add some predefined SYSTEM labels. + * + * @param name + * @param shortName + * @param description + * @return + */ + @Transactional(readOnly = false) + public Board createNewBoard(String name, String shortName, String description, int projectId) { + Board board = createEmptyBoard(name, shortName, description, projectId); + + BoardColumnDefinition closedDefinition = findColumnDefinitionByProjectIdAndType(ColumnDefinition.CLOSED, + projectId); + BoardColumnDefinition backlogDefinition = findColumnDefinitionByProjectIdAndType(ColumnDefinition.BACKLOG, + projectId); + // Add the ARCHIVE and BACKLOG columns + boardColumnRepository.addColumnToBoard(BoardColumnLocation.ARCHIVE.toString(), closedDefinition.getId(), + BoardColumnLocation.ARCHIVE, board.getId()); + boardColumnRepository.addColumnToBoard(BoardColumnLocation.BACKLOG.toString(), backlogDefinition.getId(), + BoardColumnLocation.BACKLOG, board.getId()); + boardColumnRepository.addColumnToBoard(BoardColumnLocation.TRASH.toString(), closedDefinition.getId(), + BoardColumnLocation.TRASH, board.getId()); + return board; + } + + @Transactional(readOnly = false) + public Board updateBoard(int boardId, String name, String description, boolean archived) { + queries.updateBoard(boardId, name, description, archived); + return queries.findBoardById(boardId); + } + + public Integer findBoardIdByShortName(String shortName) { + return queries.findBoardIdByShortName(shortName); + } + + public Board findBoardByShortName(String shortName) { + return queries.findBoardByShortName(shortName); + } + + public boolean existsWithShortName(String shortName) { + return Integer.valueOf(1).equals(queries.existsWithShortName(shortName)); + } + + public Board findBoardById(int boardId) { + return queries.findBoardById(boardId); + } + + public List findAll() { + return queries.findAll(); + } + + public List findBoardInfo(int projectId) { + return queries.findBoardInfo(projectId); + } + + public BoardColumnDefinition findColumnDefinitionByProjectIdAndType(ColumnDefinition definition, int projectId) { + return queries.findColumnDefinitionByProjectIdAndType(projectId, definition.toString()); + } + + public ProjectAndBoard findProjectAndBoardByColumnId(int columnId) { + return queries.findProjectAndBoardByColumnId(columnId); + } +} diff --git a/src/main/java/io/lavagna/service/BulkOperationService.java b/src/main/java/io/lavagna/service/BulkOperationService.java new file mode 100644 index 000000000..5cdbe5b44 --- /dev/null +++ b/src/main/java/io/lavagna/service/BulkOperationService.java @@ -0,0 +1,295 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.CardFull; +import io.lavagna.model.CardLabel; +import io.lavagna.model.Project; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.LabelAndValue; +import io.lavagna.model.User; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.ImmutablePair; + +@Service +@Transactional(readOnly = false) +public class BulkOperationService { + + private final CardRepository cardRepository; + private final CardLabelRepository cardLabelRepository; + private final ProjectService projectService; + private final LabelService labelService; + + @Autowired + public BulkOperationService(CardRepository cardRepository, CardLabelRepository cardLabelRepository, + LabelService labelService, ProjectService projectService) { + this.cardRepository = cardRepository; + this.cardLabelRepository = cardLabelRepository; + this.labelService = labelService; + this.projectService = projectService; + } + + public List assign(String projectShortName, List cardIds, LabelValue value, User user) { + + List filteredCardIds = keepCardIdsInProject(cardIds, projectShortName); + int labelId = findBy(projectShortName, "ASSIGNED", LabelDomain.SYSTEM).getId(); + + // we remove the cards that have _already_ the user assigned + Collection alreadyWithUserAssigned = keepCardWithMatching(filteredCardIds, + new FilterByLabelIdAndLabelValue(labelId, value)).keySet(); + filteredCardIds.removeAll(alreadyWithUserAssigned); + // + + labelService.addLabelValueToCards(labelId, filteredCardIds, value, user, new Date()); + return filteredCardIds; + } + + public List removeAssign(String projectShortName, List cardIds, LabelValue value, User user) { + List filteredCardIds = keepCardIdsInProject(cardIds, projectShortName); + int labelId = findBy(projectShortName, "ASSIGNED", LabelDomain.SYSTEM).getId(); + + List removedIds = new ArrayList<>(); + for (LabelAndValue lv : flatten(keepCardWithMatching(filteredCardIds, + new FilterByLabelIdAndLabelValue(labelId, value)).values())) { + labelService.removeLabelValue(lv.labelValue(), user, new Date()); + removedIds.add(lv.getLabelValueCardId()); + } + + return removedIds; + } + + public List reAssign(String projectShortName, List cardIds, LabelValue value, User user) { + List filteredCardIds = keepCardIdsInProject(cardIds, projectShortName); + int labelId = findBy(projectShortName, "ASSIGNED", LabelDomain.SYSTEM).getId(); + + // remove all assigned labels + for (LabelAndValue lv : flatten(keepCardWithMatching(filteredCardIds, new FilterByLabelId(labelId)).values())) { + labelService.removeLabelValue(lv.labelValue(), user, new Date()); + } + // + labelService.addLabelValueToCards(labelId, filteredCardIds, value, user, new Date()); + return filteredCardIds; + } + + public ImmutablePair, List> setDueDate(String projectShortName, List cardIds, + LabelValue value, User user) { + return addLabelOrUpdate(projectShortName, cardIds, value, user, "DUE_DATE", LabelDomain.SYSTEM); + } + + public List removeDueDate(String projectShortName, List cardIds, User user) { + return removeLabelWithName(projectShortName, cardIds, user, "DUE_DATE", LabelDomain.SYSTEM); + } + + public ImmutablePair, List> setMilestone(String projectShortName, List cardIds, + LabelValue value, User user) { + return addLabelOrUpdate(projectShortName, cardIds, value, user, "MILESTONE", LabelDomain.SYSTEM); + } + + public List removeMilestone(String projectShortName, List cardIds, User user) { + return removeLabelWithName(projectShortName, cardIds, user, "MILESTONE", LabelDomain.SYSTEM); + } + + public List removeLabel(String projectShortName, int labelId, List cardIds, User user) { + CardLabel cl = cardLabelRepository.findLabelById(labelId); + Validate.isTrue(cl.getDomain() == LabelDomain.USER); + Project p = projectService.findByShortName(projectShortName); + Validate.isTrue(cl.getProjectId() == p.getId()); + return removeLabelWithName(projectShortName, cardIds, user, cl.getName(), LabelDomain.USER); + } + + public List addLabel(String projectShortName, Integer labelId, LabelValue value, List cardIds, + User user) { + + CardLabel cl = cardLabelRepository.findLabelById(labelId); + Validate.isTrue(cl.getDomain() == LabelDomain.USER); + Project p = projectService.findByShortName(projectShortName); + Validate.isTrue(cl.getProjectId() == p.getId()); + + List filteredCardIds = keepCardIdsInProject(cardIds, projectShortName); + + Collection alreadyWithLabel = keepCardWithMatching(filteredCardIds, + new FilterByLabelIdAndLabelValueAndUniqueness(labelId, value)).keySet(); + filteredCardIds.removeAll(alreadyWithLabel); + // + + labelService.addLabelValueToCards(labelId, filteredCardIds, value, user, new Date()); + return filteredCardIds; + } + + private List removeLabelWithName(String projectShortName, List cardIds, User user, + String labelName, LabelDomain labelDomain) { + List affected = new ArrayList<>(); + List filteredCardIds = keepCardIdsInProject(cardIds, projectShortName); + int labelId = findBy(projectShortName, labelName, labelDomain).getId(); + + for (LabelAndValue lv : flatten(keepCardWithMatching(filteredCardIds, new FilterByLabelId(labelId)).values())) { + labelService.removeLabelValue(lv.labelValue(), user, new Date()); + affected.add(lv.getLabelValueCardId()); + } + + return affected; + } + + private ImmutablePair, List> addLabelOrUpdate(String projectShortName, + List cardIds, LabelValue value, User user, String labelName, LabelDomain labelDomain) { + List filteredCardIds = keepCardIdsInProject(cardIds, projectShortName); + int labelId = findBy(projectShortName, labelName, labelDomain).getId(); + + Map> cardsWithDueDate = keepCardWithMatching(filteredCardIds, new FilterByLabelId( + labelId)); + + List updatedCardIds = new ArrayList<>(); + // to update only if the label value has changed + for (LabelAndValue lv : flatten(cardsWithDueDate.values())) { + if (!lv.labelValue().getValue().equals(value)) { + labelService.updateLabelValue(lv.labelValue().newValue(lv.getLabelType(), value), user, new Date()); + updatedCardIds.add(lv.getLabelValueCardId()); + } + } + + // to add + filteredCardIds.removeAll(cardsWithDueDate.keySet()); + labelService.addLabelValueToCards(labelId, filteredCardIds, value, user, new Date()); + return ImmutablePair.of(updatedCardIds, filteredCardIds); + } + + private Map> keepCardWithMatching(List cardIds, FilterLabelAndValue filter) { + Map> res = new HashMap<>(); + for (Entry> kv : cardLabelRepository.findCardLabelValuesByCardIds(cardIds) + .entrySet()) { + List matchingLabelIdAndLabelValue = filter.filter(kv.getValue()); + if (!matchingLabelIdAndLabelValue.isEmpty()) { + res.put(kv.getKey(), matchingLabelIdAndLabelValue); + } + } + return res; + } + + private static class FilterByLabelId implements FilterLabelAndValue { + private final int labelId; + + private FilterByLabelId(int labelId) { + this.labelId = labelId; + } + + @Override + public List filter(List lvs) { + List matching = new ArrayList<>(); + for (LabelAndValue lv : lvs) { + if (lv.getLabelId() == labelId) { + matching.add(lv); + } + } + return matching; + } + } + + /** + * Keep a list of all the cards that already have a label assigned (if it's a unique label) or a label+value + * combination + */ + private static class FilterByLabelIdAndLabelValueAndUniqueness implements FilterLabelAndValue { + + private final int labelId; + private final LabelValue value; + + private FilterByLabelIdAndLabelValueAndUniqueness(int labelId, LabelValue value) { + this.labelId = labelId; + this.value = value; + } + + @Override + public List filter(List lvs) { + List matching = new ArrayList<>(); + for (LabelAndValue lv : lvs) { + if (lv.getLabelId() == labelId && (lv.isLabelUnique() || lv.getValue().equals(value))) { + matching.add(lv); + } + } + return matching; + } + + } + + private static class FilterByLabelIdAndLabelValue implements FilterLabelAndValue { + + private final int labelId; + private final LabelValue value; + + private FilterByLabelIdAndLabelValue(int labelId, LabelValue value) { + this.labelId = labelId; + this.value = value; + } + + @Override + public List filter(List lvs) { + List matching = new ArrayList<>(); + for (LabelAndValue lv : lvs) { + if (lv.getLabelId() == labelId && lv.getValue().equals(value)) { + matching.add(lv); + } + } + return matching; + } + } + + private interface FilterLabelAndValue { + List filter(List lvs); + } + + private static List flatten(Collection> cc) { + List res = new ArrayList<>(); + for (Collection c : cc) { + res.addAll(c); + } + return res; + } + + private List keepCardIdsInProject(List ids, String projectShortName) { + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + List res = new ArrayList<>(ids.size()); + for (CardFull cf : cardRepository.findAllByIds(ids)) { + if (projectShortName.equals(cf.getProjectShortName())) { + res.add(cf.getId()); + } + } + return res; + } + + private CardLabel findBy(String shortName, String name, LabelDomain labelDomain) { + return cardLabelRepository + .findLabelByName(projectService.findByShortName(shortName).getId(), name, labelDomain); + } + +} diff --git a/src/main/java/io/lavagna/service/CardDataRepository.java b/src/main/java/io/lavagna/service/CardDataRepository.java new file mode 100644 index 000000000..b060fd12f --- /dev/null +++ b/src/main/java/io/lavagna/service/CardDataRepository.java @@ -0,0 +1,332 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.trimToEmpty; +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.CardData; +import io.lavagna.model.CardDataCount; +import io.lavagna.model.CardDataFull; +import io.lavagna.model.CardDataIdAndOrder; +import io.lavagna.model.CardDataMetadata; +import io.lavagna.model.CardDataUploadContentInfo; +import io.lavagna.model.CardIdAndContent; +import io.lavagna.model.CardType; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.FileDataLight; +import io.lavagna.model.User; +import io.lavagna.query.CardDataQuery; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StreamUtils; + +@Repository +@Transactional(readOnly = true) +public class CardDataRepository { + + private static final Logger LOG = LogManager.getLogger(); + + private final NamedParameterJdbcTemplate jdbc; + private final CardDataQuery queries; + + @Autowired + public CardDataRepository(NamedParameterJdbcTemplate jdbc, CardDataQuery queries) { + this.jdbc = jdbc; + this.queries = queries; + } + + private static List toStringList(Set s) { + List r = new ArrayList<>(s.size()); + for (Object e : s) { + r.add(e.toString()); + } + return r; + } + + // prepare a {:order, :id, :cardId} list + private static List prepareOrderParameter(List dataIds, int cardId, Integer referenceId) { + LOG.debug("prepareOrderParameter: {card: {}, data count: {}, reference {}}", cardId, dataIds.size(), + referenceId); + List params = new ArrayList<>(dataIds.size()); + for (int i = 0; i < dataIds.size(); i++) { + LOG.debug("prepareOrderParameter FOR: {data: {}, order: {}}", dataIds.get(i), i + 1); + SqlParameterSource p = new MapSqlParameterSource("order", i + 1).addValue("id", dataIds.get(i)) + .addValue("cardId", cardId).addValue("referenceId", referenceId); + params.add(p); + } + return params; + } + + public CardData getUndeletedDataLightById(int id) { + return queries.getUndeletedDataLightById(id); + } + + public CardData getDataLightById(int id) { + return queries.getDataLightById(id); + } + + public Map findDataByIds(Collection ids) { + if (ids.isEmpty()) { + return Collections.emptyMap(); + } + Map res = new HashMap<>(); + for (CardIdAndContent c : queries.findDataByIds(ids)) { + res.put(c.getId(), c.getContent()); + } + return res; + } + + public CardDataMetadata findMetadataById(int id) { + return queries.findMetadataById(id); + } + + public String findContentWith(int cardId, int referenceId, CardType type, int order) { + List res = queries.findContentWith(cardId, referenceId, type.toString(), order); + return res.isEmpty() ? null : res.get(0).getContent(); + } + + public List findAllDataLightByCardId(int cardId) { + return queries.findAllLightByCardId(cardId); + } + + public List findAllDataLightByReferenceId(int referenceId) { + return queries.findAllLightByReferenceId(referenceId); + } + + public List findAllDataLightByCardIdAndTypes(int cardId, Set types) { + return queries.findAllLightByCardIdAndTypes(cardId, toStringList(types)); + } + + public List findAllDataLightByCardIdAndType(int cardId, CardType type) { + return queries.findAllLightByCardIdAndType(cardId, type.toString()); + } + + public List findAllByTypes(Set types) { + return queries.findAllCardDataIdAndOrderByType(toStringList(types)); + } + + public List findAllDataLightByReferenceIdAndType(int referenceId, CardType type) { + return queries.findAllLightByReferenceIdAndType(referenceId, type.toString()); + } + + public List findAllDataByCardIdAndType(int cardId, CardType type) { + return queries.findAllByCardIdAndType(cardId, type.toString()); + } + + public List findAllFilesByCardId(int cardId) { + return queries.findAllFilesByCardId(cardId); + } + + public FileDataLight getUndeletedFileByCardDataId(int cardDataId) { + return queries.getUndeletedFileByCardDataId(cardDataId); + } + + public List findAllDataUploadContentInfo() { + return queries.findAllDataUploadContentInfo(); + } + + @Transactional(readOnly = false) + public CardData createData(int cardId, CardType type, String content) { + LOG.debug("createCardData: {card: {}, type: {}, content: {}}", cardId, type, content); + queries.create(cardId, type.toString(), requireNonNull(trimToEmpty(content), "content cannot be empty")); + return queries.findLastCreatedLight(); + } + + @Transactional(readOnly = false) + public CardData createDataWithReferenceOrder(int cardId, Integer referenceId, CardType type, String content) { + LOG.debug("createDataWithReferenceOrder: {card: {}, reference: {}, type: {}, content: {}}", cardId, + referenceId, type, content); + + queries.createWithReferenceOrder(cardId, referenceId, type.toString(), + requireNonNull(trimToEmpty(content), "content cannot be empty")); + + return queries.findLastCreatedLight(); + } + + /** + * Order the action list. Additionally, the ids are filtered. + * + * @param cardId + * @param data + */ + @Transactional(readOnly = false) + public void updateActionListOrder(int cardId, List data) { + + // we filter out wrong ids + List filtered = Utils.filter(data, + queries.findAllCardDataIdsBy(data, cardId, CardType.ACTION_LIST.toString())); + // + + SqlParameterSource[] params = new SqlParameterSource[filtered.size()]; + for (int i = 0; i < filtered.size(); i++) { + params[i] = new MapSqlParameterSource("order", i + 1).addValue("id", filtered.get(i)).addValue("cardId", + cardId); + } + + jdbc.batchUpdate(queries.updateOrder(), params); + } + + @Transactional(readOnly = false) + public int updateOrderById(int id, int order) { + return queries.updateOrderById(id, order); + } + + /** + * Order the action item inside a action list. Additionally, the ids are filtered. + * + * @param cardId + * @param newReferenceId + * @param newDataOrder + */ + @Transactional(readOnly = false) + public void updateOrderByCardAndReferenceId(int cardId, Integer newReferenceId, List newDataOrder) { + + List filtered = Utils.filter( + newDataOrder, + queries.findAllCardDataIdsBy(newDataOrder, cardId, newReferenceId, + Arrays.asList(CardType.ACTION_CHECKED.toString(), CardType.ACTION_UNCHECKED.toString()))); + + List params = prepareOrderParameter(filtered, cardId, newReferenceId); + + jdbc.batchUpdate(queries.updateOrderByCardAndReferenceId(), + params.toArray(new SqlParameterSource[params.size()])); + } + + @Transactional(readOnly = false) + public int updateReferenceId(int cardId, int dataId, Integer referenceId) { + LOG.debug("updateReferenceId: {card: {}, data: {}, referenceId: {}}", cardId, dataId, referenceId); + return queries.updateReferenceId(referenceId, dataId, cardId); + } + + @Transactional(readOnly = false) + public int updateContent(int id, Set types, String content) { + return queries.updateContent(requireNonNull(trimToEmpty(content), "content cannot be empty"), id, + toStringList(types)); + } + + @Transactional(readOnly = false) + public int updateType(int id, Set oldTypes, CardType newType, User user) { + LOG.debug("updateType: {item: {}, type: {}}", id, newType); + return queries.updateType(newType.toString(), id, toStringList(oldTypes)); + } + + @Transactional(readOnly = false) + public int softDelete(int id, Set types) { + LOG.debug("softDelete: {id: {}}", id); + return queries.softDelete(id, toStringList(types)); + } + + @Transactional(readOnly = false) + public int undoSoftDelete(int id, Set types) { + LOG.debug("undoSoftDelete: {id: {}}", id); + return queries.undoSoftDelete(id, toStringList(types)); + } + + @Transactional(readOnly = false) + public int softDeleteOnCascade(int id, Set types) { + LOG.debug("softDeleteOnCascade: {id: {}}", id); + return queries.softDeleteOnCascade(id, toStringList(types)); + } + + @Transactional(readOnly = false) + public int undoSoftDeleteOnCascade(int id, Set types, Set filteredEvents) { + LOG.debug("undoSoftDeleteOnCascade: {id: {}}", id); + return queries.undoSoftDeleteOnCascade(id, toStringList(types), toStringList(filteredEvents)); + } + + public List findCountsByBoardIdAndLocation(int boardId, BoardColumnLocation location) { + return queries.findCountsByBoardIdAndLocation(boardId, location.toString()); + } + + public List findCountsByCardIds(List ids) { + return queries.findCountsByCardIds(ids); + } + + @Transactional(readOnly = false) + public int addUploadContent(final String digest, final long fileSize, final InputStream content, + final String contentType) { + LobHandler lobHandler = new DefaultLobHandler(); + return jdbc.getJdbcOperations().execute(queries.addUploadContent(), + new AbstractLobCreatingPreparedStatementCallback(lobHandler) { + + @Override + protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { + ps.setString(1, digest); + ps.setLong(2, fileSize); + lobCreator.setBlobAsBinaryStream(ps, 3, content, (int) fileSize); + ps.setString(4, contentType); + } + }); + } + + @Transactional(readOnly = false) + public int createUploadInfo(String digest, String name, String displayName, int cardDataId) { + return queries.mapUploadContent(cardDataId, digest, name, displayName); + } + + public boolean fileExists(String digest) { + return queries.findDigest(digest).equals(1); + } + + public boolean isFileAvailableByCard(String digest, int cardId) { + return queries.isFileAvailableByCard(cardId, digest).equals(1); + } + + public void outputFileContent(String digest, final OutputStream out) throws IOException { + LOG.debug("get file digest : {} ", digest); + SqlParameterSource param = new MapSqlParameterSource("digest", digest); + + jdbc.query(queries.fileContent(), param, new RowCallbackHandler() { + @Override + public void processRow(ResultSet rs) throws SQLException { + try (InputStream is = rs.getBinaryStream("CONTENT")) { + StreamUtils.copy(is, out); + } catch (IOException e) { + throw new IllegalStateException("Error while copying data", e); + } + } + }); + } +} diff --git a/src/main/java/io/lavagna/service/CardDataService.java b/src/main/java/io/lavagna/service/CardDataService.java new file mode 100644 index 000000000..ba5ceb10e --- /dev/null +++ b/src/main/java/io/lavagna/service/CardDataService.java @@ -0,0 +1,257 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static java.util.EnumSet.of; +import io.lavagna.model.CardData; +import io.lavagna.model.CardDataFull; +import io.lavagna.model.CardType; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.FileDataLight; +import io.lavagna.model.User; + +import java.io.InputStream; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class CardDataService { + + private final EventRepository eventRepository; + private final CardDataRepository cardDataRepository; + + @Autowired + public CardDataService(EventRepository eventRepository, CardDataRepository cardDataRepository) { + this.eventRepository = eventRepository; + this.cardDataRepository = cardDataRepository; + } + + public List findDescriptionByCardId(int cardId) { + return cardDataRepository.findAllDataByCardIdAndType(cardId, CardType.DESCRIPTION); + } + + public List findAllCommentsByCardId(int cardId) { + return cardDataRepository.findAllDataByCardIdAndType(cardId, CardType.COMMENT); + } + + public List findAllActionListsAndItemsByCardId(int cardId) { + return cardDataRepository.findAllDataLightByCardIdAndTypes(cardId, + of(CardType.ACTION_CHECKED, CardType.ACTION_UNCHECKED, CardType.ACTION_LIST)); + } + + @Transactional(readOnly = false) + private CardData createDescription(int cardId, String content, Date time, User user) { + CardData description = cardDataRepository.createData(cardId, CardType.DESCRIPTION, content); + eventRepository.insertCardDataEvent(description.getId(), cardId, EventType.DESCRIPTION_CREATE, user.getId(), + description.getId(), time); + return description; + } + + @Transactional(readOnly = false) + public CardData createComment(int cardId, String content, Date time, User user) { + CardData comment = cardDataRepository.createData(cardId, CardType.COMMENT, content); + eventRepository.insertCardDataEvent(comment.getId(), cardId, EventType.COMMENT_CREATE, user.getId(), + comment.getId(), time); + return comment; + } + + @Transactional(readOnly = false) + public CardData createActionList(int cardId, String name, User user, Date time) { + CardData actionList = cardDataRepository.createData(cardId, CardType.ACTION_LIST, name); + eventRepository.insertCardDataEvent(actionList.getId(), cardId, EventType.ACTION_LIST_CREATE, user.getId(), + actionList.getId(), time); + return actionList; + } + + @Transactional(readOnly = false) + public CardData createActionItem(int cardId, int actionListId, String name, User user, Date time) { + CardData actionItem = cardDataRepository.createDataWithReferenceOrder(cardId, actionListId, + CardType.ACTION_UNCHECKED, name); + eventRepository.insertCardDataEvent(actionItem.getId(), cardId, EventType.ACTION_ITEM_CREATE, user.getId(), + actionItem.getReferenceId(), time); + return actionItem; + } + + @Transactional(readOnly = false) + public ImmutablePair createFile(String name, String digest, long fileSize, int cardId, + InputStream content, String contentType, User user, Date time) { + if (!cardDataRepository.fileExists(digest)) { + cardDataRepository.addUploadContent(digest, fileSize, content, contentType); + } + if (!cardDataRepository.isFileAvailableByCard(digest, cardId)) { + CardData file = cardDataRepository.createData(cardId, CardType.FILE, digest); + cardDataRepository.createUploadInfo(digest, name, name, file.getId()); + eventRepository.insertFileEvent(file.getId(), cardId, EventType.FILE_UPLOAD, user.getId(), file.getId(), + name, time); + return ImmutablePair.of(true, file); + } + return ImmutablePair.of(false, null); + } + + /** + * Checked and filtered. + * + * @param cardId + * @param dataId + * @param newReferenceId + * @param newDataOrder + * @param user + * @param time + */ + @Transactional(readOnly = false) + public void moveActionItem(int cardId, int dataId, Integer newReferenceId, List newDataOrder, User user, + Date time) { + CardData actionItem = cardDataRepository.getUndeletedDataLightById(dataId); + + CardData newActionList = cardDataRepository.getDataLightById(newReferenceId); + + // checks + Validate.isTrue(actionItem.getCardId() == cardId + && (actionItem.getType() == CardType.ACTION_CHECKED || actionItem.getType() == CardType.ACTION_UNCHECKED)); + Validate.isTrue(newActionList.getCardId() == cardId && newActionList.getType() == CardType.ACTION_LIST); + // + + cardDataRepository.updateReferenceId(cardId, dataId, newReferenceId); + + cardDataRepository.updateOrderByCardAndReferenceId(cardId, newReferenceId, newDataOrder); + eventRepository.insertCardDataEvent(dataId, cardId, EventType.ACTION_ITEM_MOVE, user.getId(), + actionItem.getReferenceId(), newReferenceId, time); + } + + @Transactional(readOnly = false) + public int toggleActionItem(int actionitemId, boolean status, User user, Date time) { + CardData actionItem = cardDataRepository.getUndeletedDataLightById(actionitemId); + eventRepository.insertCardDataEvent(actionitemId, actionItem.getCardId(), status ? EventType.ACTION_ITEM_CHECK + : EventType.ACTION_ITEM_UNCHECK, user.getId(), actionItem.getReferenceId(), time); + return cardDataRepository.updateType(actionitemId, of(CardType.ACTION_CHECKED, CardType.ACTION_UNCHECKED), + status ? CardType.ACTION_CHECKED : CardType.ACTION_UNCHECKED, user); + } + + @Transactional(readOnly = false) + public int updateActionItem(int actionItemId, String content) { + return cardDataRepository.updateContent(actionItemId, of(CardType.ACTION_CHECKED, CardType.ACTION_UNCHECKED), + content); + } + + @Transactional(readOnly = false) + public int updateActionList(int actionListId, String content) { + return cardDataRepository.updateContent(actionListId, of(CardType.ACTION_LIST), content); + } + + @Transactional(readOnly = false) + public int updateComment(int commentId, String content, Date time, User user) { + CardData comment = cardDataRepository.getUndeletedDataLightById(commentId); + // save old comment as history and update the reference id of the event + CardData historyComment = cardDataRepository.createDataWithReferenceOrder(comment.getCardId(), comment.getId(), + CardType.COMMENT_HISTORY, comment.getContent()); + // insert new data + eventRepository.insertCardDataEvent(commentId, comment.getCardId(), EventType.COMMENT_UPDATE, user.getId(), + historyComment.getId(), time); + return cardDataRepository.updateContent(commentId, of(CardType.COMMENT), content); + } + + @Transactional(readOnly = false) + public int updateDescription(int cardId, String content, Date time, User user) { + // if no data is returned, then create the description + List descriptions = cardDataRepository.findAllDataLightByCardIdAndType(cardId, CardType.DESCRIPTION); + if (descriptions.isEmpty()) { + return createDescription(cardId, content, time, user).getId(); + } + + // there can (should) be only one + Validate.isTrue(1 == descriptions.size()); + CardData description = descriptions.get(0); + // save old description as history and update the reference id of the + // event + CardData historyDescription = cardDataRepository.createDataWithReferenceOrder(description.getCardId(), + description.getId(), CardType.DESCRIPTION_HISTORY, description.getContent()); + // insert new data + eventRepository.insertCardDataEvent(description.getId(), description.getCardId(), EventType.DESCRIPTION_UPDATE, + user.getId(), historyDescription.getId(), time); + return cardDataRepository.updateContent(description.getId(), of(CardType.DESCRIPTION), content); + } + + @Transactional(readOnly = false) + public Event deleteActionItem(int actionItemId, User user, Date time) { + CardData actionItem = cardDataRepository.getUndeletedDataLightById(actionItemId); + Event event = eventRepository.insertCardDataEvent(actionItemId, actionItem.getCardId(), + EventType.ACTION_ITEM_DELETE, user.getId(), actionItem.getReferenceId(), time); + cardDataRepository.softDelete(actionItemId, of(CardType.ACTION_CHECKED, CardType.ACTION_UNCHECKED)); + return event; + } + + @Transactional(readOnly = false) + public Event deleteActionList(int actionListId, User user, Date time) { + CardData actionList = cardDataRepository.getUndeletedDataLightById(actionListId); + Validate.isTrue(actionList.getType() == CardType.ACTION_LIST); + Event event = eventRepository.insertCardDataEvent(actionListId, actionList.getCardId(), + EventType.ACTION_LIST_DELETE, user.getId(), actionListId, time); + cardDataRepository.softDeleteOnCascade(actionListId, of(CardType.ACTION_LIST)); + return event; + } + + @Transactional(readOnly = false) + public Event deleteComment(int commentId, User user, Date time) { + CardData comment = cardDataRepository.getUndeletedDataLightById(commentId); + Event event = eventRepository.insertCardDataEvent(commentId, comment.getCardId(), EventType.COMMENT_DELETE, + user.getId(), commentId, time); + cardDataRepository.softDelete(commentId, of(CardType.COMMENT)); + return event; + } + + @Transactional(readOnly = false) + public Event deleteFile(int cardDataId, User user, Date time) { + FileDataLight file = cardDataRepository.getUndeletedFileByCardDataId(cardDataId); + Event event = eventRepository.insertFileEvent(file.getCardDataId(), file.getCardId(), EventType.FILE_DELETE, + user.getId(), file.getCardDataId(), file.getName(), time); + cardDataRepository.softDelete(file.getCardDataId(), of(CardType.FILE)); + return event; + } + + @Transactional(readOnly = false) + public void undoDeleteActionItem(Event event) { + eventRepository.remove(event.getId(), event.getCardId(), event.getEvent()); + cardDataRepository.undoSoftDelete(event.getDataId(), of(CardType.ACTION_CHECKED, CardType.ACTION_UNCHECKED)); + } + + @Transactional(readOnly = false) + public void undoDeleteComment(Event event) { + eventRepository.remove(event.getId(), event.getCardId(), event.getEvent()); + cardDataRepository.undoSoftDelete(event.getDataId(), of(CardType.COMMENT)); + } + + @Transactional(readOnly = false) + public void undoDeleteActionList(Event event) { + eventRepository.remove(event.getId(), event.getCardId(), event.getEvent()); + cardDataRepository.undoSoftDeleteOnCascade(event.getDataId(), of(CardType.ACTION_LIST), + of(EventType.ACTION_ITEM_DELETE)); + } + + @Transactional(readOnly = false) + public void undoDeleteFile(Event event) { + eventRepository.remove(event.getId(), event.getCardId(), event.getEvent()); + cardDataRepository.undoSoftDelete(event.getDataId(), of(CardType.FILE)); + } +} diff --git a/src/main/java/io/lavagna/service/CardLabelRepository.java b/src/main/java/io/lavagna/service/CardLabelRepository.java new file mode 100644 index 000000000..d055bf3c7 --- /dev/null +++ b/src/main/java/io/lavagna/service/CardLabelRepository.java @@ -0,0 +1,272 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Label; +import io.lavagna.model.LabelAndValue; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.UserWithPermission; +import io.lavagna.query.CardLabelQuery; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional(readOnly = true) +public class CardLabelRepository { + + private final NamedParameterJdbcTemplate jdbc; + private final CardLabelQuery queries; + + @Autowired + public CardLabelRepository(NamedParameterJdbcTemplate jdbc, CardLabelQuery queries) { + this.jdbc = jdbc; + this.queries = queries; + } + + @Transactional(readOnly = false) + public void addSystemLabels(int projectId) { + queries.addSystemLabels(projectId); + } + + @Transactional(readOnly = false) + public CardLabel addLabel(int projectId, boolean unique, LabelType labelType, LabelDomain labelDomain, String name, + int color) { + final boolean reservedName = CardLabel.RESERVED_SYSTEM_LABELS_NAME.contains(name); + Validate.isTrue((labelDomain == LabelDomain.SYSTEM && reservedName) + || (labelDomain == LabelDomain.USER && !reservedName), name + " is a reserved system label name"); + + queries.addLabel(projectId, unique, labelType.toString(), labelDomain.toString(), name, color); + + return queries.findLastCreatedLabel(); + } + + @Transactional(readOnly = false) + public void removeLabel(int labelId) { + queries.removeLabelListValues(labelId); + queries.removeLabel(labelId); + } + + public List findLabelsByProject(int projectId) { + return queries.findLabelsByProject(projectId); + } + + public CardLabel findLabelById(int labelId) { + return queries.findLabelById(labelId); + } + + public CardLabel findLabelByName(int projectId, String labelName, LabelDomain labelDomain) { + return queries.findLabelByName(projectId, labelName, labelDomain.toString()); + } + + public List findLabelsByName(int projectId, String labelName, LabelDomain labelDomain) { + return queries.findLabelsByName(projectId, labelName, labelDomain.toString()); + } + + public CardLabelValue findLabelValueById(int labelValueId) { + return queries.findLabelValueById(labelValueId); + } + + public List findLabelValueByLabelAndValue(int cardId, CardLabel cl, LabelValue lv) { + return queries.findLabelValueByLabelAndValue(cardId, cl.getId(), lv.getValueString(), lv.getValueTimestamp(), + lv.getValueInt(), lv.getValueCard(), lv.getValueUser(), lv.getValueList()); + } + + /** + * Return a map of label by {cardId => {CardLabel => [CardLabelValue]}} + * + * @return + */ + public Map>> findCardLabelValuesByBoardId(int boardId, + BoardColumnLocation location) { + + Map>> res = new HashMap<>(); + for (LabelAndValue lv : queries.findCardLabelValuesByBoardId(boardId, location.toString())) { + if (!res.containsKey(lv.getLabelValueCardId())) { + res.put(lv.getLabelValueCardId(), new HashMap>()); + } + CardLabel cl = lv.label(); + if (!res.get(lv.getLabelValueCardId()).containsKey(cl)) { + res.get(lv.getLabelValueCardId()).put(cl, new ArrayList()); + } + res.get(lv.getLabelValueCardId()).get(cl).add(lv.labelValue()); + } + return res; + } + + public Map> findCardLabelValuesByCardIds(List ids) { + Map> res = new HashMap<>(); + for (LabelAndValue lv : queries.findCardLabelValuesByCardIds(ids)) { + if (!res.containsKey(lv.getLabelValueCardId())) { + res.put(lv.getLabelValueCardId(), new ArrayList()); + } + res.get(lv.getLabelValueCardId()).add(lv); + } + return res; + } + + public Map> findCardLabelValuesByCardId(int cardId) { + + Map> res = new HashMap<>(); + + for (LabelAndValue lv : queries.findCardLabelValuesByCardId(cardId)) { + CardLabel cl = lv.label(); + if (!res.containsKey(cl)) { + res.put(cl, new ArrayList()); + } + res.get(cl).add(lv.labelValue()); + } + + return res; + } + + @Transactional(readOnly = false) + public CardLabel updateLabel(int labelId, Label label) { + CardLabel cl = findLabelById(labelId); + Validate.isTrue(cl.getDomain() == LabelDomain.USER, "Cannot update values in SYSTEM label for label with id " + + labelId); + return updateLabel(label, cl); + } + + @Transactional(readOnly = false) + public CardLabel updateSystemLabel(int labelId, Label label) { + CardLabel cl = findLabelById(labelId); + Validate.isTrue(cl.getDomain() == LabelDomain.SYSTEM, "Cannot update values in USER label for label with id " + + labelId); + return updateLabel(label, cl); + } + + public List findUserLabelNameBy(String term, Integer projectId, UserWithPermission userWithPermission) { + Set projectIdFilter = userWithPermission.toProjectIdsFilter(projectId); + return projectIdFilter.isEmpty() ? queries.findUserLabelNameBy(term) : queries.findUserLabelNameBy(term, projectIdFilter); + } + + public List findListValuesBy(LabelDomain domain, String labelName, String term, Integer projectId, UserWithPermission userWithPermission) { + Set projectIdFilter = userWithPermission.toProjectIdsFilter(projectId); + return projectIdFilter.isEmpty() ? queries.findListValuesBy(domain.toString(), labelName, term) : queries.findListValuesBy(domain.toString(), labelName, term, projectIdFilter); + } + + + @Transactional(readOnly = false) + private CardLabel updateLabel(Label label, CardLabel cl) { + // type cannot be changed! + Validate.isTrue(cl.getType() == label.getType()); + + CardLabel toUpdate = cl.set(label.getName(), label.getType(), label.getColor()); + + queries.updateLabel(toUpdate.getName(), toUpdate.getColor(), toUpdate.getType().toString(), toUpdate.getId()); + + return toUpdate; + } + + @Transactional(readOnly = false) + public CardLabelValue addLabelValueToCard(CardLabel label, int cardId, LabelValue val) { + + queries.addLabelValueToCard(cardId, label.isUnique() ? true : null, label.getId(), label.getType().toString(), + val.getValueString(), val.getValueTimestamp(), val.getValueInt(), val.getValueCard(), + val.getValueUser(), val.getValueList()); + + return queries.findLastCreatedLabelValue(); + } + + @Transactional(readOnly = false) + public int removeLabelValue(CardLabelValue cardLabelValue) { + return queries.removeLabelValue(cardLabelValue.getCardLabelValueId()); + } + + // Label list values + + @Transactional(readOnly = false) + public LabelListValue addLabelListValue(int labelId, String value) { + queries.addLabelListValue(labelId, value); + return queries.findLastCreatedLabelListValue(); + } + + @Transactional(readOnly = false) + public void removeLabelListValue(int labelListValueId) { + queries.removeLabelListValue(labelListValueId); + } + + @Transactional(readOnly = false) + public void updateLabelListValue(LabelListValue lvl) { + queries.updateLabelListValue(lvl.getId(), lvl.getValue()); + } + + public List findListValuesByLabelId(int labelId) { + return queries.findListValuesByLabelId(labelId); + } + + public List findListValuesByLabelIdAndValue(int labelId, String value) { + return queries.findListValuesByLabelIdAndValue(labelId, value); + } + + public LabelListValue findListValueById(int labelListValueId) { + return queries.findListValueById(labelListValueId); + } + + @Transactional(readOnly = false) + public void swapLabelListValues(int first, int second) { + LabelListValue firstValue = findListValueById(first); + LabelListValue secondValue = findListValueById(second); + SqlParameterSource p1 = new MapSqlParameterSource("id", firstValue.getId()).addValue("order", + secondValue.getOrder()); + SqlParameterSource p2 = new MapSqlParameterSource("id", secondValue.getId()).addValue("order", + firstValue.getOrder()); + jdbc.batchUpdate(queries.swapLabelListValues(), new SqlParameterSource[] { p1, p2 }); + } + + public Map> findLabelListValueMapping(List labelValues) { + final Map> res = new HashMap<>(); + jdbc.query(queries.findLabelListValueMapping(), new MapSqlParameterSource("values", labelValues), + new RowCallbackHandler() { + @Override + public void processRow(ResultSet rs) throws SQLException { + String name = rs.getString("CARD_LABEL_LIST_VALUE"); + if (!res.containsKey(name)) { + res.put(name, new HashMap()); + } + res.get(name).put(rs.getInt("CARD_LABEL_ID_FK"), rs.getInt("CARD_LABEL_LIST_VALUE_ID")); + } + }); + return res; + } + + public int labelUsedCount(int labelId) { + return queries.labelUsedCount(labelId); + } + +} diff --git a/src/main/java/io/lavagna/service/CardRepository.java b/src/main/java/io/lavagna/service/CardRepository.java new file mode 100644 index 000000000..01765c264 --- /dev/null +++ b/src/main/java/io/lavagna/service/CardRepository.java @@ -0,0 +1,311 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.StringUtils.trimToNull; +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.Card; +import io.lavagna.model.CardFull; +import io.lavagna.model.Event; +import io.lavagna.model.User; +import io.lavagna.query.CardQuery; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional(readOnly = true) +public class CardRepository { + + private static final Logger LOG = LogManager.getLogger(); + + private final NamedParameterJdbcTemplate jdbc; + private final CardQuery queries; + + @Autowired + public CardRepository(NamedParameterJdbcTemplate jdbc, CardQuery queries) { + this.jdbc = jdbc; + this.queries = queries; + } + + // prepare a {:cardOrder, :cardId, :columnId} list + private static List prepareOrderParameter(List cardIds, int columnId) { + List params = new ArrayList<>(cardIds.size()); + for (int i = 0; i < cardIds.size(); i++) { + SqlParameterSource p = new MapSqlParameterSource("cardOrder", i + 1)// + .addValue("cardId", cardIds.get(i))// + .addValue("columnId", columnId); + params.add(p); + } + return params; + } + + public List findAllByBoardShortName(String boardShortName) { + return queries.findAllByBoardShortName(boardShortName); + } + + public List findAllByBoardIdAndLocation(int boardId, BoardColumnLocation location) { + return queries.findAllByBoardIdAndLocation(boardId, location.toString()); + } + + public List findAllByColumnId(int columnId) { + return queries.findAllFullByColumnId(columnId); + } + + public List findAllByIds(Collection ids) { + return ids.isEmpty() ? Collections. emptyList() : queries.findAllByIds(ids); + } + + public List fetchAllOpenCardsByUserId(int userId, int page, int pageSize) { + return queries.fetchAllOpenCardsByUserId(userId, pageSize + 1, page * pageSize); + } + + public List fetchAllOpenCardsByProjectAndUserId(String projectShortName, int userId, int page, + int pageSize) { + return queries.fetchAllOpenCardsByProjectIdAndUserId(userId, projectShortName, pageSize + 1, page * pageSize); + } + + public List findCards(int boardId, String criteria) { + return queries.findCards(boardId, criteria); + } + + public List fetchAllActivityByCardId(int cardId) { + return queries.fetchAllActivityByCardId(cardId); + } + + public List fetchPaginatedByBoardIdAndLocation(int boardId, BoardColumnLocation location, int page) { + return queries.fetchPaginatedByBoardIdAndLocation(boardId, location.toString(), 11, page * 10); + } + + public Card findBy(int cardId) { + return queries.findBy(cardId); + } + + public CardFull findFullBy(int cardId) { + return queries.findFullBy(cardId); + } + + public CardFull findFullBy(String boardShortName, int seqNumber) { + return queries.findFullBy(boardShortName, seqNumber); + } + + public Integer findCardIdByBoardNameAndSeq(String boardShortName, int seqNumber) { + return queries.findCardIdByBoardNameAndSeq(boardShortName, seqNumber); + } + + public boolean existCardWith(String boardShortName, int seqNumber) { + return Integer.valueOf(1).equals(queries.countCardIdByBoardNameAndSeq(boardShortName, seqNumber)); + } + + public Card updateCard(int cardId, String name, User user) { + queries.updateCard(trimToNull(name), cardId); + return findBy(cardId); + } + + /** + * Returns the new Card + * + * @param name + * @param columnId + * @return + */ + @Transactional(readOnly = false) + public Card createCard(String name, int columnId, User user) { + + LOG.debug("createCard: {name: {}, columnId: {}, userId: {}}", name, columnId, user.getId()); + + int sequence = fetchAndLockSequence(columnId); + queries.createCard(trimToNull(name), columnId, user.getId(), sequence); + incrementSequence(columnId, sequence); + return queries.findLastCreatedCard(); + } + + @Transactional(readOnly = false) + public Card createCardFromTop(String name, int columnId, User user) { + Card createdCard = createCard(name, columnId, user); + moveLastCardAtTop(createdCard.getId(), columnId); + return createdCard; + } + + @Transactional(readOnly = false) + private void moveLastCardAtTop(int lastCardId, int columnId) { + queries.incrementCardsOrder(columnId); + SqlParameterSource updateParam = new MapSqlParameterSource("columnId", columnId).addValue("cardId", lastCardId) + .addValue("cardOrder", 0); + jdbc.update(queries.updateCardOrder(), updateParam); + } + + /** + * Fetch the ticket number from the counter and lock the row. + * + * @param columnId + * @return + */ + private int fetchAndLockSequence(int columnId) { + return queries.fetchAndLockCardSequence(columnId); + } + + /** + * Increment the counter + * + * @param columnId + */ + @Transactional(readOnly = false) + private void incrementSequence(int columnId, int sequence) { + int affected = queries.incrementSequence(sequence, columnId); + Validate.isTrue(affected == 1, "during the update sequence, " + affected + + " were affected for a card inserted in the columnId " + columnId); + } + + /** + * move a card and update the order of the new column. The ids are filtered. + * + * @param id + * @param prevColumnId + * @param newColumnId + * @param newOrderForNewColumn + */ + @Transactional(readOnly = false) + public void moveCardToColumnAndReorder(int id, int prevColumnId, int newColumnId, List newOrderForNewColumn) { + moveCardToColumn(id, prevColumnId, newColumnId); + updateCardOrder(newOrderForNewColumn, newColumnId); + } + + @Transactional(readOnly = false) + public void moveCardToColumn(int cardId, int previousColumnId, int columnId) { + + SqlParameterSource param = new MapSqlParameterSource("cardId", cardId).addValue("columnId", columnId).addValue( + "previousColumnId", previousColumnId); + int affected = jdbc.update(queries.moveCardToColumn(), param); + Validate.isTrue(1 == affected, "moveCardToColumn: must affect exactly one row"); + } + + @Transactional(readOnly = false) + public List moveCardsToColumn(List cardIds, int previousColumnId, int columnId, int userId) { + + List filteredCardIds = Utils.filter(cardIds, queries.findCardIdsInColumnId(cardIds, previousColumnId)); + + List params = new ArrayList<>(filteredCardIds.size()); + for (int cardId : filteredCardIds) { + SqlParameterSource p = new MapSqlParameterSource("cardId", cardId)// + .addValue("previousColumnId", previousColumnId)// + .addValue("columnId", columnId); + params.add(p); + } + + int[] updateResult = jdbc.batchUpdate(queries.moveCardToColumn(), + params.toArray(new SqlParameterSource[params.size()])); + + List updated = new ArrayList<>(); + for (int i = 0; i < updateResult.length; i++) { + if (updateResult[i] > 0) { + updated.add(filteredCardIds.get(i)); + } + } + + return updated; + } + + /** + * Update card order in a given column id. The cardIds are filtered. + * + * @param cardIds + * @param columnId + */ + @Transactional(readOnly = false) + public void updateCardOrder(List cardIds, int columnId) { + + if (cardIds.isEmpty()) { + return; + } + + List filteredCardIds = Utils.filter(cardIds, queries.findCardIdsInColumnId(cardIds, columnId)); + + List params = prepareOrderParameter(filteredCardIds, columnId); + jdbc.batchUpdate(queries.updateCardOrder(), params.toArray(new SqlParameterSource[params.size()])); + } + + public Map findCardsIds(List cards) { + + List param = new ArrayList<>(cards.size()); + for (String card : cards) { + String[] splitted = StringUtils.split(card, '-'); + if (splitted.length > 1) { + try { + Integer cardSequenceNumber = Integer.valueOf(splitted[splitted.length - 1], 10); + String boardShortName = StringUtils + .join(ArrayUtils.subarray(splitted, 0, splitted.length - 1), '-'); + param.add(new Object[] { boardShortName, cardSequenceNumber }); + + } catch (NumberFormatException nfe) { + // skip + } + } + } + + if (param.isEmpty()) { + return Collections.emptyMap(); + } + + final Map res = new HashMap<>(); + MapSqlParameterSource paramS = new MapSqlParameterSource("projShortNameAndCardSeq", param); + jdbc.query(queries.findCardsIs(), paramS, new RowCallbackHandler() { + @Override + public void processRow(ResultSet rs) throws SQLException { + res.put(rs.getString("CARD_IDENTIFIER"), rs.getInt("CARD_ID")); + } + }); + + return res; + } + + public List findCardBy(String term, Set projectIds) { + return projectIds == null ? queries.findCardBy(term) : queries.findCardBy(term, projectIds); + } + + public int getOpenCardsCountByUserId(int id) { + return queries.getOpenCardsCountByUserId(id); + } + + public int getOpenCardsCountByProjectAndUserId(String projectShortName, int id) { + return queries.getOpenCardsCountByProjectAndUserId(projectShortName, id); + } + + public int updateCardOrder(int cardId, int order) { + return queries.updateCardOrder(cardId, order); + } +} diff --git a/src/main/java/io/lavagna/service/CardService.java b/src/main/java/io/lavagna/service/CardService.java new file mode 100644 index 000000000..510661fb0 --- /dev/null +++ b/src/main/java/io/lavagna/service/CardService.java @@ -0,0 +1,191 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.Card; +import io.lavagna.model.CardDataCount; +import io.lavagna.model.CardFull; +import io.lavagna.model.CardFullWithCounts; +import io.lavagna.model.CardFullWithCountsHolder; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.LabelAndValue; +import io.lavagna.model.User; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class CardService { + + private final EventRepository eventRepository; + private final CardRepository cardRepository; + private final CardDataRepository cardDataRepository; + private final CardLabelRepository cardLabelRepository; + + @Autowired + public CardService(CardRepository cardRepository, CardDataRepository cardDataRepository, + EventRepository eventRepository, CardLabelRepository cardLabelRepository) { + this.cardRepository = cardRepository; + this.eventRepository = eventRepository; + this.cardDataRepository = cardDataRepository; + this.cardLabelRepository = cardLabelRepository; + } + + private static List fetchIds(List cards) { + List r = new ArrayList<>(cards.size()); + for (CardFull c : cards) { + r.add(c.getId()); + } + return r; + } + + public List fetchAllInColumn(int columnId) { + + List cards = cardRepository.findAllByColumnId(columnId); + if (cards.isEmpty()) { + return Collections.emptyList(); + } + List res = fetchCardFull(cards); + + Collections.sort(res, new Comparator() { + @Override + public int compare(CardFullWithCounts o1, CardFullWithCounts o2) { + return new CompareToBuilder().append(o1.getOrder(), o2.getOrder()).toComparison(); + } + }); + // + return res; + } + + public CardFullWithCountsHolder getAllOpenCards(User user, int page, int pageSize) { + + List cards = cardRepository.fetchAllOpenCardsByUserId(user.getId(), page, pageSize); + if (cards.isEmpty()) { + return new CardFullWithCountsHolder(Collections. emptyList(), 0, 0); + } + List res = fetchCardFull(cards); + + int totalItems = 0; + if ((page == 0 && cards.size() > pageSize) || (page > 0 && !cards.isEmpty())) { + totalItems = cardRepository.getOpenCardsCountByUserId(user.getId()); + } else { + totalItems = cards.size(); + } + + return new CardFullWithCountsHolder(res, totalItems, pageSize); + } + + public CardFullWithCountsHolder getAllOpenCardsByProject(String projectShortName, User user, int page, int pageSize) { + List cards = cardRepository.fetchAllOpenCardsByProjectAndUserId(projectShortName, user.getId(), page, + pageSize); + if (cards.isEmpty()) { + return new CardFullWithCountsHolder(Collections. emptyList(), 0, 0); + } + List res = fetchCardFull(cards); + + int totalItems = 0; + if ((page == 0 && cards.size() > pageSize) || (page > 0 && !cards.isEmpty())) { + totalItems = cardRepository.getOpenCardsCountByProjectAndUserId(projectShortName, user.getId()); + } else { + totalItems = cards.size(); + } + + return new CardFullWithCountsHolder(res, totalItems, pageSize); + } + + List fetchCardFull(List cards) { + List ids = fetchIds(cards); + Map> counts = aggregateByCardId(cardDataRepository.findCountsByCardIds(ids)); + Map> labels = cardLabelRepository.findCardLabelValuesByCardIds(ids); + List res = new ArrayList<>(); + for (CardFull card : cards) { + res.add(new CardFullWithCounts(card, counts.get(card.getId()), labels.get(card.getId()))); + } + return res; + } + + @Transactional(readOnly = false) + public void moveCardsToColumn(List cardIds, int previousColumnId, int columnId, int userId, + EventType boardEventType, Date time) { + List updated = cardRepository.moveCardsToColumn(cardIds, previousColumnId, columnId, userId); + eventRepository.insertCardEvents(updated, previousColumnId, columnId, userId, boardEventType, time, null); + } + + @Transactional(readOnly = false) + public Event updateCard(int cardId, String name, User user, Date date) { + Card card = cardRepository.updateCard(cardId, name, user); + return eventRepository.insertCardEvent(cardId, card.getColumnId(), user.getId(), EventType.CARD_UPDATE, date, + name); + } + + @Transactional(readOnly = false) + public Card createCard(String name, int columnId, Date creationTime, User user) { + Card card = cardRepository.createCard(name, columnId, user); + eventRepository.insertCardEvent(card.getId(), columnId, user.getId(), EventType.CARD_CREATE, creationTime, + card.getName()); + return card; + } + + @Transactional(readOnly = false) + public Card createCardFromTop(String name, int columnId, Date creationTime, User user) { + Card card = cardRepository.createCardFromTop(name, columnId, user); + eventRepository.insertCardEvent(card.getId(), columnId, user.getId(), EventType.CARD_CREATE, creationTime, + card.getName()); + return card; + } + + @Transactional(readOnly = false) + public Event moveCardToColumn(int cardId, int previousColumnId, int columnId, int userId, Date date) { + cardRepository.moveCardToColumn(cardId, previousColumnId, columnId); + return eventRepository.insertCardEvent(cardId, previousColumnId, columnId, userId, EventType.CARD_MOVE, date, + null); + } + + @Transactional(readOnly = false) + public Event moveCardToColumnAndReorder(int cardId, int prevColumnId, int newColumnId, + List newOrderForNewColumn, User user) { + cardRepository.moveCardToColumnAndReorder(cardId, prevColumnId, newColumnId, newOrderForNewColumn); + return eventRepository.insertCardEvent(cardId, prevColumnId, newColumnId, user.getId(), EventType.CARD_MOVE, + new Date(), null); + } + + private static Map> aggregateByCardId(List counts) { + Map> r = new TreeMap<>(); + + for (CardDataCount c : counts) { + if (!r.containsKey(c.getCardId())) { + r.put(c.getCardId(), new TreeMap()); + } + r.get(c.getCardId()).put(c.getType(), c); + } + + return r; + } + +} diff --git a/src/main/java/io/lavagna/service/ConfigurationRepository.java b/src/main/java/io/lavagna/service/ConfigurationRepository.java new file mode 100644 index 000000000..47a69abbd --- /dev/null +++ b/src/main/java/io/lavagna/service/ConfigurationRepository.java @@ -0,0 +1,106 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.ConfigurationKeyValue; +import io.lavagna.model.Key; +import io.lavagna.query.ConfigurationQuery; + +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional(readOnly = true) +public class ConfigurationRepository { + + private final ConfigurationQuery queries; + + @Autowired + public ConfigurationRepository(ConfigurationQuery queries) { + this.queries = queries; + } + + public List findAll() { + return queries.findAll(); + } + + public Map findConfigurationFor(Set keys) { + Set s = new HashSet<>(); + Map res = new EnumMap(Key.class); + for (Key k : keys) { + s.add(k.toString()); + res.put(k, null); + } + + for (ConfigurationKeyValue kv : queries.findConfigurationFor(s)) { + res.put(kv.getFirst(), kv.getSecond()); + } + return res; + } + + public boolean hasKeyDefined(Key key) { + return !Integer.valueOf(0).equals(queries.hasKeyDefined(key.toString())); + } + + public String getValueOrNull(Key key) { + List res = queries.getValue(key.toString()); + return res.isEmpty() ? null : res.get(0); + } + + public String getValue(Key key) { + List res = queries.getValue(key.toString()); + if (res.isEmpty()) { + throw new EmptyResultDataAccessException(1); + } else { + return res.get(0); + } + } + + @Transactional(readOnly = false) + public void insert(Key key, String value) { + queries.set(key.toString(), value); + } + + @Transactional(readOnly = false) + public void update(Key key, String value) { + queries.update(key.toString(), value); + } + + @Transactional(readOnly = false) + public void delete(Key key) { + queries.delete(key.toString()); + } + + @Transactional(readOnly = false) + public void updateOrCreate(List toUpdateOrCreate) { + for (ConfigurationKeyValue kv : toUpdateOrCreate) { + if (hasKeyDefined(kv.getFirst())) { + update(kv.getFirst(), kv.getSecond()); + } else { + insert(kv.getFirst(), kv.getSecond()); + } + } + } +} diff --git a/src/main/java/io/lavagna/service/DatabaseMigrator.java b/src/main/java/io/lavagna/service/DatabaseMigrator.java new file mode 100644 index 000000000..b71a590ea --- /dev/null +++ b/src/main/java/io/lavagna/service/DatabaseMigrator.java @@ -0,0 +1,50 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import javax.sql.DataSource; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationVersion; +import org.springframework.core.env.Environment; + +public class DatabaseMigrator { + + public DatabaseMigrator(Environment env, DataSource dataSource, MigrationVersion target) { + if (canMigrate(env)) { + doMigration(env, dataSource, target); + } + } + + private void doMigration(Environment env, DataSource dataSource, MigrationVersion version) { + String sqlDialect = env.getRequiredProperty("datasource.dialect"); + Flyway migration = new Flyway(); + migration.setDataSource(dataSource); + // FIXME remove the validation = false when the schemas will be stable + migration.setValidateOnMigrate(false); + // + + migration.setTarget(version); + + migration.setLocations("io/lavagna/db/" + sqlDialect + "/"); + migration.migrate(); + } + + private boolean canMigrate(Environment env) { + return !"true".equals(env.getProperty("datasource.disable.migration")); + } +} diff --git a/src/main/java/io/lavagna/service/EventEmitter.java b/src/main/java/io/lavagna/service/EventEmitter.java new file mode 100644 index 000000000..5393f2070 --- /dev/null +++ b/src/main/java/io/lavagna/service/EventEmitter.java @@ -0,0 +1,418 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.CardFull; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.lang3.tuple.Triple; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Component; + +@Component +public class EventEmitter { + + private final SimpMessageSendingOperations messagingTemplate; + + @Autowired + public EventEmitter(SimpMessageSendingOperations messageSendingOperations) { + this.messagingTemplate = messageSendingOperations; + } + + private static Event event(LavagnaEvent type) { + return new Event(type, null); + } + + private static ImportEvent importEvent(int currentBoard, int boards, String boardName) { + return new ImportEvent(currentBoard, boards, boardName); + } + + private static Event event(LavagnaEvent type, Object payload) { + return new Event(type, payload); + } + + private static String columnDestination(String boardShortName, BoardColumnLocation location) { + return "/event/board/" + boardShortName + "/location/" + location + "/column"; + } + + private static String column(int columnId) { + return "/event/column/" + columnId + "/card"; + } + + private static String board(String projectShortName, String boardShortName) { + return "/event/" + projectShortName + "/" + boardShortName + "/card"; + } + + private static String cardData(int cardId) { + return "/event/card/" + cardId + "/card-data"; + } + + // ------------ project + + public void emitCreateProject(String projectShortName) { + messagingTemplate.convertAndSend("/event/project", event(LavagnaEvent.CREATE_PROJECT, projectShortName)); + } + + public void emitUpdateProject(String projectShortName) { + messagingTemplate.convertAndSend("/event/project", event(LavagnaEvent.UPDATE_PROJECT, projectShortName)); + } + + public void emitImportProject(String importId, int currentBoard, int boards, String boardName) { + messagingTemplate.convertAndSend("/event/import/" + importId, importEvent(currentBoard, boards, boardName)); + } + + // ------------ board + + public void emitCreateBoard(String projectShortName) { + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/board", + event(LavagnaEvent.CREATE_BOARD)); + } + + public void emitUpdateBoard(String boardShortName) { + messagingTemplate.convertAndSend("/event/board/" + boardShortName, event(LavagnaEvent.UPDATE_BOARD)); + } + + // ------------ column + + public void emitCreateColumn(String boardShortName, BoardColumnLocation location) { + messagingTemplate + .convertAndSend(columnDestination(boardShortName, location), event(LavagnaEvent.CREATE_COLUMN)); + } + + public void emitUpdateColumn(String boardShortName, BoardColumnLocation location, int columnId) { + messagingTemplate + .convertAndSend(columnDestination(boardShortName, location), event(LavagnaEvent.UPDATE_COLUMN)); + messagingTemplate.convertAndSend("/event/column/" + columnId, event(LavagnaEvent.UPDATE_COLUMN)); + } + + public void emitUpdateColumnPosition(String boardShortName, BoardColumnLocation location) { + messagingTemplate.convertAndSend(columnDestination(boardShortName, location), + event(LavagnaEvent.UPDATE_COLUMN_POSITION)); + } + + // ------------ card + + public void emitCreateCard(String projectShortName, String boardShortName, int columnId, int cardId) { + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.CREATE_CARD)); + messagingTemplate.convertAndSend(board(projectShortName, boardShortName), + event(LavagnaEvent.CREATE_CARD, cardId)); + } + + public void emitUpdateCard(String projectShortName, String boardShortName, int columnId, int cardId) { + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UPDATE_CARD)); + messagingTemplate.convertAndSend(board(projectShortName, boardShortName), + event(LavagnaEvent.UPDATE_CARD, cardId)); + } + + public void emitUpdateCardPosition(int columnId) { + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UPDATE_CARD_POSITION)); + } + + public void emitMoveCardOutsideOfBoard(String boardShortName, BoardColumnLocation location) { + messagingTemplate.convertAndSend("/event/board/" + boardShortName + "/location/" + location + "/card", + event(LavagnaEvent.UPDATE_CARD_POSITION)); + } + + public void emitMoveCardFromOutsideOfBoard(String boardShortName, BoardColumnLocation location) { + messagingTemplate.convertAndSend("/event/board/" + boardShortName + "/location/" + location + "/card", + event(LavagnaEvent.UPDATE_CARD_POSITION)); + } + + public void emitCardHasMoved(String projectShortName, String boardShortName, Collection affected) { + for (Integer a : affected) { + messagingTemplate.convertAndSend(board(projectShortName, boardShortName), + event(LavagnaEvent.UPDATE_CARD_POSITION, a)); + } + } + + public void emitCreateRole() { + messagingTemplate.convertAndSend("/event/permission", event(LavagnaEvent.CREATE_ROLE)); + } + + // ------------ permission + + public void emitCreateRole(String projectShortName) { + messagingTemplate.convertAndSend("/event/permission/project/" + projectShortName, + event(LavagnaEvent.CREATE_ROLE)); + } + + public void emitDeleteRole() { + messagingTemplate.convertAndSend("/event/permission", event(LavagnaEvent.DELETE_ROLE)); + } + + public void emitDeleteRole(String projectShortName) { + messagingTemplate.convertAndSend("/event/permission/project/" + projectShortName, + event(LavagnaEvent.DELETE_ROLE)); + } + + public void emitUpdatePermissionsToRole() { + messagingTemplate.convertAndSend("/event/permission", event(LavagnaEvent.UPDATE_PERMISSION_TO_ROLE)); + } + + public void emitUpdatePermissionsToRole(String projectShortName) { + messagingTemplate.convertAndSend("/event/permission/project/" + projectShortName, + event(LavagnaEvent.UPDATE_PERMISSION_TO_ROLE)); + } + + public void emitAssignRoleToUsers(String role) { + messagingTemplate.convertAndSend("/event/permission", event(LavagnaEvent.ASSIGN_ROLE_TO_USERS, role)); + } + + public void emitAssignRoleToUsers(String role, String projectShortName) { + messagingTemplate.convertAndSend("/event/permission/project/" + projectShortName, + event(LavagnaEvent.ASSIGN_ROLE_TO_USERS, role)); + } + + public void emitRemoveRoleToUsers(String role) { + messagingTemplate.convertAndSend("/event/permission", event(LavagnaEvent.REMOVE_ROLE_TO_USERS, role)); + } + + public void emitRemoveRoleToUsers(String role, String projectShortName) { + messagingTemplate.convertAndSend("/event/permission/project/" + projectShortName, + event(LavagnaEvent.REMOVE_ROLE_TO_USERS, role)); + } + + // ------------ card description + public void emitUpdateDescription(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UPDATE_DESCRIPTION)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UPDATE_DESCRIPTION)); + } + + // ------------ comment + public void emitCreateComment(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.CREATE_COMMENT)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.CREATE_COMMENT)); + } + + public void emitUpdateComment(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UPDATE_COMMENT)); + } + + public void emitDeleteComment(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.DELETE_COMMENT)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.DELETE_COMMENT)); + } + + public void emitUndoDeleteComment(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UNDO_DELETE_COMMENT)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UNDO_DELETE_COMMENT)); + } + + // ------------ action list handling + + public void emitCreateActionList(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.CREATE_ACTION_LIST)); + } + + public void emitDeleteActionList(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.DELETE_ACTION_LIST)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.DELETE_ACTION_LIST)); + } + + public void emitUpdateActionList(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UPDATE_ACTION_LIST)); + } + + public void emitReorderActionLists(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.REORDER_ACTION_LIST)); + } + + public void emitCreateActionItem(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.CREATE_ACTION_ITEM)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.REORDER_ACTION_LIST)); + } + + public void emitDeleteActionItem(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.DELETE_ACTION_ITEM)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.DELETE_ACTION_ITEM)); + } + + public void emitToggleActionItem(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.TOGGLE_ACTION_ITEM)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.TOGGLE_ACTION_ITEM)); + } + + public void emitUpdateUpdateActionItem(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UPDATE_ACTION_ITEM)); + } + + public void emitMoveActionItem(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.MOVE_ACTION_ITEM)); + } + + public void emitReorderActionItems(int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.REORDER_ACTION_ITEM)); + } + + public void emiteUndoDeleteActionItem(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UNDO_DELETE_ACTION_ITEM)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UNDO_DELETE_ACTION_ITEM)); + } + + public void emitUndoDeleteActionList(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UNDO_DELETE_ACTION_LIST)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UNDO_DELETE_ACTION_LIST)); + + } + + // ------------ + public void emitUploadFile(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.CREATE_FILE)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.CREATE_FILE)); + } + + public void emitDeleteFile(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.DELETE_FILE)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.DELETE_FILE)); + } + + public void emiteUndoDeleteFile(int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UNDO_DELETE_FILE)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UNDO_DELETE_FILE)); + } + + // ------------ + public void emitAddLabelValueToCard(String projectShortName, int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.ADD_LABEL_VALUE_TO_CARD)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.ADD_LABEL_VALUE_TO_CARD)); + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/label-value", + event(LavagnaEvent.ADD_LABEL_VALUE_TO_CARD)); + } + + public void emitUpdateLabelValue(String projectShortName, int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.UPDATE_LABEL_VALUE)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.UPDATE_LABEL_VALUE)); + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/label-value", + event(LavagnaEvent.UPDATE_LABEL_VALUE)); + } + + public void emitRemoveLabelValue(String projectShortName, int columnId, int cardId) { + messagingTemplate.convertAndSend(cardData(cardId), event(LavagnaEvent.REMOVE_LABEL_VALUE)); + messagingTemplate.convertAndSend(column(columnId), event(LavagnaEvent.REMOVE_LABEL_VALUE)); + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/label-value", + event(LavagnaEvent.REMOVE_LABEL_VALUE)); + } + + private static Triple, Set, Set> extractFrom(List l) { + Set cardIds = new HashSet<>(); + Set columnIds = new HashSet<>(); + Set projectShortNames = new HashSet<>(); + for (CardFull cf : l) { + cardIds.add(cf.getId()); + columnIds.add(cf.getColumnId()); + projectShortNames.add(cf.getProjectShortName()); + } + return Triple.of(cardIds, columnIds, projectShortNames); + } + + public void emitRemoveLabelValueToCards(List affectedCards) { + sendEventForLabel(affectedCards, LavagnaEvent.REMOVE_LABEL_VALUE); + } + + private void sendEventForLabel(List affectedCards, LavagnaEvent ev) { + Triple, Set, Set> a = extractFrom(affectedCards); + for (int cardId : a.getLeft()) { + messagingTemplate.convertAndSend(cardData(cardId), event(ev)); + } + for (int columnId : a.getMiddle()) { + messagingTemplate.convertAndSend(column(columnId), event(ev)); + } + for (String projectShortName : a.getRight()) { + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/label-value", event(ev)); + } + } + + public void emitAddLabelValueToCards(List affectedCards) { + sendEventForLabel(affectedCards, LavagnaEvent.ADD_LABEL_VALUE_TO_CARD); + } + + public void emitUpdateOrAddValueToCards(List updated, List added) { + sendEventForLabel(updated, LavagnaEvent.UPDATE_LABEL_VALUE); + sendEventForLabel(added, LavagnaEvent.ADD_LABEL_VALUE_TO_CARD); + } + + public void emitAddLabel(String projectShortName) { + messagingTemplate + .convertAndSend("/event/project/" + projectShortName + "/label", event(LavagnaEvent.ADD_LABEL)); + } + + public void emitUpdateLabel(String projectShortName, int labelId) { + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/label", + event(LavagnaEvent.UPDATE_LABEL, labelId)); + } + + public void emitDeleteLabel(String projectShortName, int labelId) { + messagingTemplate.convertAndSend("/event/project/" + projectShortName + "/label", + event(LavagnaEvent.DELETE_LABEL, labelId)); + } + + // user profile update + public void emitUpdateUserProfile(int userId) { + messagingTemplate.convertAndSend("/event/user", event(LavagnaEvent.UPDATE_USER, userId)); + } + + private enum LavagnaEvent { + CREATE_PROJECT, CREATE_BOARD, // + UPDATE_PROJECT, UPDATE_BOARD, // + CREATE_COLUMN, UPDATE_COLUMN, UPDATE_COLUMN_POSITION, // + CREATE_CARD, UPDATE_CARD, // + UPDATE_CARD_POSITION, // + + // role and permission + CREATE_ROLE, DELETE_ROLE, UPDATE_PERMISSION_TO_ROLE, ASSIGN_ROLE_TO_USERS, REMOVE_ROLE_TO_USERS, + // + UPDATE_DESCRIPTION, + // + CREATE_COMMENT, UPDATE_COMMENT, DELETE_COMMENT, UNDO_DELETE_COMMENT, + // + CREATE_ACTION_LIST, DELETE_ACTION_LIST, UPDATE_ACTION_LIST, REORDER_ACTION_LIST, UNDO_DELETE_ACTION_LIST, + // + CREATE_ACTION_ITEM, DELETE_ACTION_ITEM, TOGGLE_ACTION_ITEM, UPDATE_ACTION_ITEM, MOVE_ACTION_ITEM, REORDER_ACTION_ITEM, UNDO_DELETE_ACTION_ITEM, + // + CREATE_FILE, DELETE_FILE, UNDO_DELETE_FILE, + // + ADD_LABEL_VALUE_TO_CARD, UPDATE_LABEL_VALUE, REMOVE_LABEL_VALUE, UNDO_REMOVE_LABEL_VALUE, ADD_LABEL, UPDATE_LABEL, DELETE_LABEL, + // + UPDATE_USER; + } + + // ------------ + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Event { + private final LavagnaEvent type; + private final Object payload; + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class ImportEvent { + private final int currentBoard; + private final int boards; + private final String boardName; + } +} diff --git a/src/main/java/io/lavagna/service/EventRepository.java b/src/main/java/io/lavagna/service/EventRepository.java new file mode 100644 index 000000000..aaf1df3c7 --- /dev/null +++ b/src/main/java/io/lavagna/service/EventRepository.java @@ -0,0 +1,177 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.EventsCount; +import io.lavagna.query.EventQuery; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional(readOnly = true) +public class EventRepository { + + private final NamedParameterJdbcTemplate jdbc; + private final EventQuery queries; + + private static final int FEED_SIZE = 20; + + @Autowired + public EventRepository(NamedParameterJdbcTemplate jdbc, EventQuery queries) { + this.jdbc = jdbc; + this.queries = queries; + } + + public int count() { + return queries.count(); + } + + public List find(int offset, int amount) { + return queries.find(offset, amount); + } + + public List findNextEventFor(Event e) { + return queries.findNextEventFor(e.getDataId(), e.getId(), e.getEvent().toString()); + } + + public Event getEventById(int eventId) { + return queries.getById(eventId); + } + + @Transactional(readOnly = false) + public Event insertLabelEvent(String labelName, int cardId, int userId, EventType event, LabelValue value, + LabelType labelType, Date time) { + + queries.insertLabelEvent(labelName, labelType.toString(), cardId, userId, time, event.toString(), + value.getValueInt(), value.getValueString(), value.getValueTimestamp(), value.getValueCard(), + value.getValueUser()); + + return queries.findLastCreated(); + } + + @Transactional(readOnly = false) + public void insertCardEvents(List cardIds, Integer previousColumnId, int columnId, int userId, + EventType event, Date time, String name) { + List param = new ArrayList<>(cardIds.size()); + for (Integer cardId : cardIds) { + param.add(prepareForCardEvent(cardId, previousColumnId, columnId, userId, event, time, name)); + } + jdbc.batchUpdate(queries.insertCardEvent(), param.toArray(new SqlParameterSource[param.size()])); + } + + @Transactional(readOnly = false) + public Event insertCardEvent(int cardId, Integer previousColumnId, int columnId, int userId, EventType event, + Date time, String name) { + insertCardEvents(Collections.singletonList(cardId), previousColumnId, columnId, userId, event, time, name); + return queries.findLastCreated(); + } + + private static SqlParameterSource prepareForCardEvent(int cardId, Integer previousColumnId, int columnId, + int userId, EventType event, Date time, String name) { + return new MapSqlParameterSource("cardId", cardId).addValue("previousColumnId", previousColumnId) + .addValue("columnId", columnId).addValue("time", time).addValue("userId", userId) + .addValue("event", event.toString()).addValue("valueString", name); + } + + @Transactional(readOnly = false) + public Event insertCardEvent(int cardId, int columnId, int userId, EventType event, Date time, String name) { + return insertCardEvent(cardId, null, columnId, userId, event, time, name); + } + + @Transactional(readOnly = false) + public void insertCardEvent(List cardIds, int columnId, int userId, EventType eventType, Date date) { + List params = new ArrayList<>(cardIds.size()); + for (Integer cardId : cardIds) { + params.add(prepareForCardEvent(cardId, null, columnId, userId, eventType, date, null)); + } + + jdbc.batchUpdate(queries.insertCardEvent(), params.toArray(new SqlParameterSource[] { })); + } + + @Transactional(readOnly = false) + public Event insertCardDataEvent(int cardDataId, int cardId, EventType event, int userId, Integer referenceId, + Date time) { + return insertCardDataEvent(cardDataId, cardId, event, userId, referenceId, null, time); + } + + @Transactional(readOnly = false) + public Event insertCardDataEvent(int cardDataId, int cardId, EventType event, int userId, Integer referenceId, + Integer newReferenceId, Date time) { + + queries.insertCardDataEvent(cardDataId, cardId, userId, time, event.toString(), referenceId, newReferenceId); + return queries.findLastCreated(); + } + + @Transactional(readOnly = false) + public Event insertFileEvent(int cardDataId, int cardId, EventType event, int userId, Integer referenceId, + String name, Date time) { + + queries.insertFileEvent(cardDataId, cardId, userId, time, event.toString(), referenceId, name); + return queries.findLastCreated(); + } + + public Set findUsersIdFor(int cardDataId, EventType event) { + return new HashSet<>(queries.findUsersIdForCardData(cardDataId, event.toString())); + } + + @Transactional(readOnly = false) + public void remove(int id, int cardId, EventType event) { + queries.remove(id, cardId, event.toString()); + } + + // feed + + public List getUserFeedByPage(int userId, int page) { + return queries.getUserFeedByPage(userId, FEED_SIZE + 1, page * FEED_SIZE); + } + + // profile + + public List getLatestActivityByPage(int userId, int page) { + return queries.getLatestActivityByPage(userId, FEED_SIZE + 1, page * FEED_SIZE); + } + + public List getLatestActivityByPageAndProjects(int userId, int page, Collection projects) { + return queries.getLatestActivityByPageAndProjects(userId, projects, FEED_SIZE + 1, page * FEED_SIZE); + } + + public List getUserActivityForProjects(int userId, Date fromDate, Collection projectIds) { + return projectIds.isEmpty() ? Collections.emptyList() : queries.getUserActivityByProjects( + userId, projectIds, fromDate); + } + + public List getUserActivity(int userId, Date fromDate) { + return queries.getUserActivity(userId, fromDate); + } +} diff --git a/src/main/java/io/lavagna/service/EventService.java b/src/main/java/io/lavagna/service/EventService.java new file mode 100644 index 000000000..849baaa62 --- /dev/null +++ b/src/main/java/io/lavagna/service/EventService.java @@ -0,0 +1,56 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.LabelListValue; + +import java.util.Date; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class EventService { + + private final EventRepository eventRepository; + private final CardLabelRepository labelRepository; + + @Autowired + public EventService(EventRepository eventRepository, CardLabelRepository labelRepository) { + this.eventRepository = eventRepository; + this.labelRepository = labelRepository; + } + + @Transactional(readOnly = false) + public Event insertLabelEvent(String labelName, int cardId, int userId, Event.EventType event, + CardLabelValue.LabelValue value, CardLabel.LabelType labelType, Date time) { + + if (labelType == CardLabel.LabelType.LIST) { + labelType = CardLabel.LabelType.STRING; + LabelListValue llv = labelRepository.findListValueById(value.getValueList()); + value = new CardLabelValue.LabelValue(llv.getValue(), value.getValueTimestamp(), value.getValueInt(), + value.getValueCard(), value.getValueUser(), null); + } + + return eventRepository.insertLabelEvent(labelName, cardId, userId, event, value, labelType, time); + } +} diff --git a/src/main/java/io/lavagna/service/EventsContext.java b/src/main/java/io/lavagna/service/EventsContext.java new file mode 100644 index 000000000..abffa0557 --- /dev/null +++ b/src/main/java/io/lavagna/service/EventsContext.java @@ -0,0 +1,102 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static java.lang.String.format; +import static org.apache.commons.lang3.ObjectUtils.firstNonNull; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.CardFull; +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.Event; +import io.lavagna.model.User; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Contains all the necessary data for formatting an email to the user. + */ +class EventsContext { + // aggregate the events by card id + final Map> events = new TreeMap<>(); + final Map users = new HashMap<>(); + final Map cards = new HashMap<>(); + final Map cardData; + final Map columns = new HashMap<>(); + + EventsContext(List events, List users, List cards, Map cardData, + List columns) { + this.cardData = cardData; + + for (Event e : events) { + if (!this.events.containsKey(e.getCardId())) { + this.events.put(e.getCardId(), new ArrayList()); + } + this.events.get(e.getCardId()).add(e); + } + for (User u : users) { + this.users.put(u.getId(), u); + } + for (CardFull c : cards) { + this.cards.put(c.getId(), c); + } + for (BoardColumn bc : columns) { + this.columns.put(bc.getId(), bc); + } + } + + String formatLabel(Event e) { + return new StringBuilder(e.getLabelName()).append(e.getLabelType() != LabelType.NULL ? "::" : "") + .append(formatLabelValue(e)).toString(); + } + + String formatLabelValue(Event e) { + if (e.getLabelType() == LabelType.STRING) { + return e.getValueString(); + } else if (e.getLabelType() == LabelType.INT) { + return Integer.toString(e.getValueInt()); + } else if (e.getLabelType() == LabelType.TIMESTAMP) { + return new SimpleDateFormat("dd.MM.yyyy").format(e.getValueTimestamp()); + } else if (e.getLabelType() == LabelType.CARD) { + CardFull cf = cards.get(e.getValueCard()); + return cf.getBoardShortName() + "-" + cf.getSequence(); + } else if (e.getLabelType() == LabelType.USER) { + return formatUser(e.getValueUser()); + } else { + return ""; + } + } + + String formatColumn(Integer colId) { + if (colId != null && columns.containsKey(colId)) { + BoardColumn col = columns.get(colId); + return format("%s (%s::%s)", col.getName(), col.getLocation(), col.getStatus()); + } else { + return "-"; + } + } + + String formatUser(int userId) { + User u = users.get(userId); + String name = firstNonNull(u.getDisplayName(), u.getEmail(), u.getProvider() + ":" + u.getUsername()); + return name + (u.getEmail() != null && !name.equals(u.getEmail()) ? " <" + u.getEmail() + ">" : ""); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/service/ExportImportService.java b/src/main/java/io/lavagna/service/ExportImportService.java new file mode 100644 index 000000000..8d895bfcb --- /dev/null +++ b/src/main/java/io/lavagna/service/ExportImportService.java @@ -0,0 +1,51 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Handle the export/import part of lavagna. + */ +@Service +@Transactional(readOnly = true, timeout = 500000) +public class ExportImportService { + + private final LavagnaImporter importer; + private final LavagnaExporter exporter; + + @Autowired + public ExportImportService(LavagnaExporter exporter, LavagnaImporter importer) { + this.importer = importer; + this.exporter = exporter; + } + + public void exportData(OutputStream os) throws IOException { + exporter.exportData(os); + } + + @Transactional(readOnly = false) + public void importData(boolean overrideConfiguration, Path tempFile) { + importer.importData(overrideConfiguration, tempFile); + } +} diff --git a/src/main/java/io/lavagna/service/ImportEvent.java b/src/main/java/io/lavagna/service/ImportEvent.java new file mode 100644 index 000000000..5584f1d12 --- /dev/null +++ b/src/main/java/io/lavagna/service/ImportEvent.java @@ -0,0 +1,26 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; + +import java.nio.file.Path; + +public interface ImportEvent { + void processEvent(EventFull e, ImportContext context, Path tempFile); +} diff --git a/src/main/java/io/lavagna/service/ImportService.java b/src/main/java/io/lavagna/service/ImportService.java new file mode 100644 index 000000000..faa830d52 --- /dev/null +++ b/src/main/java/io/lavagna/service/ImportService.java @@ -0,0 +1,488 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.BoardColumn; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.CardData; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.Event; +import io.lavagna.model.Project; +import io.lavagna.model.User; +import io.lavagna.web.api.model.TrelloImportRequest; +import io.lavagna.web.api.model.TrelloImportRequest.BoardIdAndShortName; +import io.lavagna.web.api.model.TrelloRequest; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +import com.julienvey.trello.Trello; +import com.julienvey.trello.domain.Action; +import com.julienvey.trello.domain.Argument; +import com.julienvey.trello.domain.Board; +import com.julienvey.trello.domain.Card; +import com.julienvey.trello.domain.CheckItem; +import com.julienvey.trello.domain.CheckList; +import com.julienvey.trello.domain.Label; +import com.julienvey.trello.domain.Member; +import com.julienvey.trello.domain.Organization; +import com.julienvey.trello.domain.TList; +import com.julienvey.trello.impl.TrelloImpl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ImportService { + + private static final Logger LOG = LogManager.getLogger(); + + private final EventEmitter eventEmitter; + private final ProjectService projectService; + private final BoardRepository boardRepository; + private final BoardColumnRepository boardColumnRepository; + private final CardDataService cardDataService; + private final CardService cardService; + private final LabelService labelService; + private final CardLabelRepository cardLabelRepository; + private final UserRepository userRepository; + + @Autowired + public ImportService(EventEmitter eventEmitter, ProjectService projectService, BoardRepository boardRepository, + BoardColumnRepository boardColumnRepository, CardDataService cardDataService, CardService cardService, + LabelService labelService, CardLabelRepository cardLabelRepository, UserRepository userRepository) { + this.eventEmitter = eventEmitter; + this.projectService = projectService; + this.boardRepository = boardRepository; + this.boardColumnRepository = boardColumnRepository; + this.cardDataService = cardDataService; + this.cardService = cardService; + this.labelService = labelService; + this.cardLabelRepository = cardLabelRepository; + this.userRepository = userRepository; + } + + public TrelloBoardsResponse getAvailableTrelloBoards(TrelloRequest request) { + TrelloBoardsResponse response = new TrelloBoardsResponse(); + Trello trello = new TrelloImpl(request.getApiKey(), request.getSecret()); + Member member = trello.getMemberInformation("me"); + Map organizations = new HashMap<>(); + for (String boardId : member.getIdBoards()) { + Board board = trello.getBoard(boardId); + if (board.isClosed()) { + continue; + } + String orgId = board.getIdOrganization(); + if (!organizations.containsKey(orgId)) { + TrelloOrganizationInfo tOrg; + if (orgId != null) { + Organization organization = board.fetchOrganization(); + tOrg = new TrelloOrganizationInfo(orgId, organization.getDisplayName()); + } else { + tOrg = new TrelloOrganizationInfo(orgId, "Personal"); + } + organizations.put(orgId, tOrg); + response.organizations.add(tOrg); + } + TrelloOrganizationInfo tOrg = organizations.get(orgId); + tOrg.boards.add(new TrelloBoardInfo(board.getId(), board.getName())); + } + return response; + } + + public TrelloImportResponse importFromTrello(TrelloImportRequest importRequest) { + Trello trello = new TrelloImpl(importRequest.getApiKey(), importRequest.getSecret()); + + // Cache the user mappings + Map lavagnaUsers = new HashMap<>(); + + TrelloImportResponse tImport = new TrelloImportResponse(); + int boardsToImport = importRequest.getBoards().size() + 1; + int currentBoard = 0; + + for (BoardIdAndShortName boardIdAndShortName : importRequest.getBoards()) { + currentBoard++; + Board board = trello.getBoard(boardIdAndShortName.getId()); + String boardShortName = boardIdAndShortName.getShortName().toUpperCase(Locale.ENGLISH); + if (board.isClosed() || boardRepository.existsWithShortName(boardShortName)) { + continue; + } + + eventEmitter.emitImportProject(importRequest.getImportId(), currentBoard, boardsToImport, board.getName()); + + TrelloBoard tBoard = new TrelloBoard(boardShortName, board.getName(), board.getDesc()); + importBoard(trello, tImport, board, tBoard, lavagnaUsers); + tImport.boards.add(tBoard); + } + eventEmitter.emitImportProject(importRequest.getImportId(), boardsToImport, boardsToImport, ""); + return tImport; + } + + private void importBoard(Trello trello, TrelloImportResponse tImport, Board board, TrelloBoard tBoard, + Map lavagnaUsers) { + + // Cache the checklists + Map checklists = new HashMap<>(); + for (CheckList checklist : trello.getBoardChecklists(board.getId())) { + checklists.put(checklist.getId(), checklist); + } + + // Cache the board members + Map boardMembers = new HashMap<>(); + for (Member member : trello.getBoardMembers(board.getId())) { + boardMembers.put(member.getId(), member); + } + + // Fetch the columns with every card in it + for (TList list : board.fetchLists(new Argument("cards", "open"))) { + TrelloBoardColumn tColumn = new TrelloBoardColumn(list.getName()); + tBoard.columns.put(list.getPos(), tColumn); + + for (Card iCard : list.getCards()) { + if (StringUtils.isEmpty(iCard.getName())) { + continue; + } + + TrelloCard tCard = new TrelloCard(iCard.getName(), iCard.getDesc(), iCard.isClosed(), iCard.getDue()); + + // Checklists + for (String checklistId : iCard.getIdChecklists()) { + CheckList checklist = checklists.get(checklistId); + TrelloChecklist tChecklist = new TrelloChecklist(checklist.getName()); + for (CheckItem iItem : checklist.getCheckItems()) { + TrelloChecklistItem item = new TrelloChecklistItem(iItem.getName(), + iItem.getState().equals("complete")); + tChecklist.items.put(iItem.getPos(), item); + } + tCard.checklists.add(tChecklist); + } + + // Comments + for (Action action : iCard.getActions(new Argument("filter", "commentCard"))) { + tCard.comments.add(new TrelloComment(action.getDate(), + getMemberFromId(lavagnaUsers, boardMembers, action.getIdMemberCreator()), + action.getData().getText())); + } + + // Assigned users + for (String memberId : iCard.getIdMembers()) { + User user = getMemberFromId(lavagnaUsers, boardMembers, memberId); + if (user != null) { + tCard.assignedUsers.add(user); + } + } + + // Labels + for (Label label : iCard.getLabels()) { + addLabelToCard(tCard, tImport, label); + } + + tColumn.cards.put(iCard.getPos(), tCard); + } + } + } + + private void addLabelToCard(TrelloCard card, TrelloImportResponse tImport, Label label) { + String name = label.getName(); + if (StringUtils.isBlank(name)) { + name = label.getColor(); + } + if (!tImport.labels.containsKey(name)) { + tImport.labels.put(name, label.getColor()); + } + card.labels.add(name); + } + + private User getMemberFromId(Map lavagnaUsers, Map boardMembers, String memberId) { + if (lavagnaUsers.containsKey(memberId)) { + return lavagnaUsers.get(memberId); + } + Member member = boardMembers.get(memberId); + User user = matchUser(lavagnaUsers, memberId, member.getUsername()); + if (user != null) + return user; + + user = matchUser(lavagnaUsers, memberId, member.getEmail()); + if (user != null) + return user; + return matchUser(lavagnaUsers, memberId, member.getFullName()); + } + + private User matchUser(Map lavagnaUsers, String memberId, String criteria) { + if (StringUtils.isNotBlank(memberId)) { + List users = userRepository.findUsers(criteria); + if (users.size() > 0) { + lavagnaUsers.put(memberId, users.get(0)); + return users.get(0); + } + } + return null; + } + + @Transactional(readOnly = false) + public void saveTrelloBoardsToDb(String projectShortName, TrelloImportResponse tImport, User user) { + Project project = projectService.findByShortName(projectShortName); + List definitions = projectService.findColumnDefinitionsByProjectId(project.getId()); + BoardColumnDefinition openDefinition = null; + for (BoardColumnDefinition def : definitions) { + if (openDefinition == null || openDefinition.getValue() != ColumnDefinition.OPEN) { + openDefinition = def; + } + } + + if (openDefinition == null) { + LOG.warn("no valid column definition has been found for the project {}.", projectShortName); + return; + } + + // Labels + Map lavagnaLabels = new HashMap<>(); + for (CardLabel cl : cardLabelRepository.findLabelsByProject(project.getId())) { + if (cl.getDomain().equals(CardLabel.LabelDomain.USER)) { + lavagnaLabels.put(cl.getName(), cl); + } + } + + for (String labelName : tImport.labels.keySet()) { + if (!lavagnaLabels.containsKey(labelName)) { + CardLabel cl = cardLabelRepository.addLabel(project.getId(), true, CardLabel.LabelType.NULL, + CardLabel.LabelDomain.USER, labelName, getColorFromTrelloColor(tImport.labels.get(labelName))); + lavagnaLabels.put(labelName, cl); + } + } + + CardLabel dueDateLabel = cardLabelRepository.findLabelByName(project.getId(), "DUE_DATE", + CardLabel.LabelDomain.SYSTEM); + + CardLabel assignedLabel = cardLabelRepository.findLabelByName(project.getId(), "ASSIGNED", + CardLabel.LabelDomain.SYSTEM); + + // Import the boards + for (TrelloBoard tBoard : tImport.boards) { + int boardId = boardRepository.createNewBoard(tBoard.getName(), tBoard.getShortName(), tBoard.getDesc(), + project.getId()).getId(); + for (Integer trelloColumnPos : tBoard.columns.keySet()) { + TrelloBoardColumn trelloColumn = tBoard.columns.get(trelloColumnPos); + + int boardColumnId = boardColumnRepository.addColumnToBoard(trelloColumn.name, openDefinition.getId(), + BoardColumn.BoardColumnLocation.BOARD, boardId).getId(); + + for (Integer pos : trelloColumn.cards.keySet()) { + TrelloCard card = trelloColumn.cards.get(pos); + importCard(card, boardId, boardColumnId, user, lavagnaLabels, dueDateLabel, assignedLabel); + } + } + } + } + + private int getColorFromTrelloColor(String color) { + switch (color) { + case "yellow": + return 16763904; + case "red": + return 15012864; + case "orange": + return 16733986; + case "green": + return 2464548; + case "pink": + return 15102392; + case "purple": + return 10233776; + case "sky": + return 48340; + case "lime": + return 13491257; + case "black": + return 0; + default: //case "blue": + return 1810914; + } + } + + private void importCard(TrelloCard card, int boardId, int boardColumnId, User user, + Map lavagnaLabels, CardLabel dueDateLabel, CardLabel assignedLabel) { + int cardId = cardService.createCard(card.getName(), boardColumnId, new Date(), user).getId(); + if (StringUtils.isNotEmpty(card.getDesc())) { + cardDataService.updateDescription(cardId, card.getDesc(), new Date(), user); + } + + // Due date + if (card.getDueDate() != null) { + labelService.addLabelValueToCard(dueDateLabel, cardId, new CardLabelValue.LabelValue(card.getDueDate()), + user, new Date()); + } + + // Checklists + for (TrelloChecklist checklist : card.checklists) { + CardData data = cardDataService.createActionList(cardId, checklist.getName(), user, new Date()); + for (Integer pos : checklist.items.keySet()) { + TrelloChecklistItem item = checklist.items.get(pos); + CardData itemData = cardDataService.createActionItem(cardId, data.getId(), item.name, user, new Date()); + if (item.isChecked) { + cardDataService.toggleActionItem(itemData.getId(), true, user, new Date()); + } + } + } + + // Comments + for (TrelloComment comment : card.comments) { + cardDataService.createComment(cardId, comment.getText(), comment.getDate(), + comment.getUser() != null ? comment.getUser() : user); + } + + // Archive if closed + if (card.isClosed()) { + BoardColumn destination = boardColumnRepository.findDefaultColumnFor(boardId, + BoardColumn.BoardColumnLocation.ARCHIVE); + List idToList = new ArrayList<>(); + idToList.add(cardId); + cardService.moveCardsToColumn(idToList, boardColumnId, destination.getId(), user.getId(), + Event.EventType.CARD_ARCHIVE, new Date()); + } + + // Assigned users + for (User assignedUser : card.assignedUsers) { + labelService.addLabelValueToCard(assignedLabel, cardId, new CardLabelValue.LabelValue(assignedUser.getId()), + user, new Date()); + } + + // Labels + for (String labelName : card.labels) { + labelService.addLabelValueToCard(lavagnaLabels.get(labelName), cardId, new CardLabelValue.LabelValue(), + user, new Date()); + } + } + + @AllArgsConstructor + static class TrelloChecklistItem { + private final String name; + private final boolean isChecked; + } + + @Getter + static class TrelloChecklist { + private final String name; + + private final Map items = new TreeMap<>(); + + public TrelloChecklist(String name) { + this.name = name; + } + } + + @Getter + @AllArgsConstructor + static class TrelloComment { + private final Date date; + private final User user; + private final String text; + } + + @Getter + static class TrelloCard { + private final String name; + private final String desc; + private final boolean isClosed; + private final Date dueDate; + + private final List comments = new ArrayList<>(); + private final List checklists = new ArrayList<>(); + private final List assignedUsers = new ArrayList<>(); + private final List labels = new ArrayList<>(); + + public TrelloCard(String name, String desc, boolean isClosed, Date dueDate) { + this.name = name; + this.desc = desc; + this.isClosed = isClosed; + this.dueDate = dueDate; + } + } + + @Getter + static class TrelloBoardColumn { + private final String name; + + private final Map cards = new TreeMap<>(); + + public TrelloBoardColumn(String name) { + this.name = name; + } + } + + @Getter + public static class TrelloBoard { + private final String shortName; + private final String name; + private final String desc; + + private final Map columns = new TreeMap<>(); + + public TrelloBoard(String shortName, String name, String desc) { + this.shortName = shortName; + this.name = name; + this.desc = desc; + } + } + + @Getter + public static class TrelloImportResponse { + private final List boards = new ArrayList<>(); + + private final Map labels = new HashMap<>(); + } + + public static class TrelloBoardsResponse { + private final List organizations = new ArrayList<>(); + } + + @AllArgsConstructor + @Getter + @Setter + public static class TrelloBoardInfo { + private final String id; + private final String name; + } + + @Getter + @Setter + public static class TrelloOrganizationInfo { + private final String id; + private final String name; + private final List boards = new ArrayList<>(); + + public TrelloOrganizationInfo(String id, String name) { + this.id = id; + this.name = name; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/service/LabelService.java b/src/main/java/io/lavagna/service/LabelService.java new file mode 100644 index 000000000..bc2eba3ad --- /dev/null +++ b/src/main/java/io/lavagna/service/LabelService.java @@ -0,0 +1,81 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.User; + +import java.util.Date; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class LabelService { + + private final EventService eventService; + private final CardLabelRepository labelRepository; + + @Autowired + public LabelService(EventService eventService, CardLabelRepository labelRepository) { + this.eventService = eventService; + this.labelRepository = labelRepository; + } + + @Transactional(readOnly = false) + public CardLabelValue addLabelValueToCard(int labelId, int cardId, LabelValue val, User user, Date time) { + return addLabelValueToCard(labelRepository.findLabelById(labelId), cardId, val, user, time); + } + + @Transactional(readOnly = false) + public CardLabelValue addLabelValueToCard(CardLabel cl, int cardId, LabelValue val, User user, Date time) { + CardLabelValue labelValue = labelRepository.addLabelValueToCard(cl, cardId, val); + eventService.insertLabelEvent(cl.getName(), cardId, user.getId(), EventType.LABEL_CREATE, val, cl.getType(), + time); + return labelValue; + } + + @Transactional(readOnly = false) + public void addLabelValueToCards(int labelId, List cardIds, LabelValue val, User user, Date time) { + CardLabel cl = labelRepository.findLabelById(labelId); + for (int cardId : cardIds) { + addLabelValueToCard(cl, cardId, val, user, time); + } + } + + @Transactional(readOnly = false) + public void updateLabelValue(CardLabelValue cardLabelValue, User user, Date time) { + CardLabel cl = labelRepository.findLabelById(cardLabelValue.getLabelId()); + removeLabelValue(labelRepository.findLabelValueById(cardLabelValue.getCardLabelValueId()), user, time); + addLabelValueToCard(cl, cardLabelValue.getCardId(), cardLabelValue.getValue(), user, time); + } + + @Transactional(readOnly = false) + public Event removeLabelValue(CardLabelValue cardLabelValue, User user, Date time) { + CardLabel cl = labelRepository.findLabelById(cardLabelValue.getLabelId()); + labelRepository.removeLabelValue(cardLabelValue); + return eventService.insertLabelEvent(cl.getName(), cardLabelValue.getCardId(), user.getId(), + EventType.LABEL_DELETE, cardLabelValue.getValue(), cl.getType(), time); + } +} diff --git a/src/main/java/io/lavagna/service/LavagnaExporter.java b/src/main/java/io/lavagna/service/LavagnaExporter.java new file mode 100644 index 000000000..a2c381398 --- /dev/null +++ b/src/main/java/io/lavagna/service/LavagnaExporter.java @@ -0,0 +1,220 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.common.Json; +import io.lavagna.model.Board; +import io.lavagna.model.BoardInfo; +import io.lavagna.model.Card; +import io.lavagna.model.CardData; +import io.lavagna.model.CardDataUploadContentInfo; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardType; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.Pair; +import io.lavagna.model.Project; +import io.lavagna.model.User; +import io.lavagna.query.StatisticsQuery; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +class LavagnaExporter { + + private final ConfigurationRepository configurationRepository; + private final UserRepository userRepository; + private final PermissionService permissionService; + private final ProjectService projectService; + private final CardLabelRepository cardLabelRepository; + private final BoardRepository boardRepository; + private final BoardColumnRepository boardColumnRepository; + private final EventRepository eventRepository; + private final CardRepository cardRepository; + private final CardDataRepository cardDataRepository; + private final StatisticsQuery statisticsQuery; + + @Autowired + public LavagnaExporter(ConfigurationRepository configurationRepository, UserRepository userRepository, + PermissionService permissionService, ProjectService projectService, + CardLabelRepository cardLabelRepository, BoardRepository boardRepository, + BoardColumnRepository boardColumnRepository, EventRepository eventRepository, + CardRepository cardRepository, CardDataRepository cardDataRepository, StatisticsQuery statisticsQuery) { + this.configurationRepository = configurationRepository; + this.userRepository = userRepository; + this.permissionService = permissionService; + this.projectService = projectService; + this.cardLabelRepository = cardLabelRepository; + this.boardRepository = boardRepository; + this.boardColumnRepository = boardColumnRepository; + this.eventRepository = eventRepository; + this.cardRepository = cardRepository; + this.cardDataRepository = cardDataRepository; + this.statisticsQuery = statisticsQuery; + } + + public void exportData(OutputStream os) throws IOException { + try (ZipOutputStream zf = new ZipOutputStream(os); + OutputStreamWriter osw = new OutputStreamWriter(zf, StandardCharsets.UTF_8)) { + writeEntry("config.json", configurationRepository.findAll(), zf, osw); + writeEntry("users.json", userRepository.findAll(), zf, osw); + writeEntry("permissions.json", permissionService.findAllRolesAndRelatedPermissionWithUsers(), zf, osw); + + exportFiles(zf, osw); + + for (Project p : projectService.findAll()) { + exportProject(zf, osw, p); + } + + // + final int amountPerPage = 100; + int pages = (eventRepository.count() + amountPerPage - 1) / amountPerPage; + + writeEntry("events-page-count.json", pages, zf, osw); + for (int i = 0; i < pages; i++) { + writeEntry("events-" + i + ".json", toEventFull(eventRepository.find(i * 100, 100)), zf, osw); + } + // + writeEntry("card-data-types-order.json", cardDataRepository.findAllByTypes(EnumSet.of(CardType.ACTION_LIST, + CardType.ACTION_CHECKED, CardType.ACTION_UNCHECKED)), zf, osw); + } + } + + private void exportFiles(ZipOutputStream zf, OutputStreamWriter osw) throws IOException { + osw.flush(); + for (CardDataUploadContentInfo fileData : cardDataRepository.findAllDataUploadContentInfo()) { + zf.putNextEntry(new ZipEntry("files/" + fileData.getDigest())); + cardDataRepository.outputFileContent(fileData.getDigest(), zf); + writeEntry("files/" + fileData.getDigest() + ".json", fileData, zf, osw); + } + } + + private List toEventFull(List events) { + List res = new ArrayList<>(events.size()); + for (Event e : events) { + // TODO not optimal + + User u = userRepository.findById(e.getUserId()); + ImmutablePair bc = findByCardId(e.getCardId()); + // + String content = handleContent(e); + + User labelUser = e.getValueUser() != null ? userRepository.findById(e.getValueUser()) : null; + ImmutablePair labelCard = e.getValueCard() != null ? findByCardId(e.getValueCard()) : null; + + // + res.add(new EventFull(e, u, bc, content, labelCard, labelUser)); + } + return res; + } + + private ImmutablePair findByCardId(int id) { + Card c = cardRepository.findBy(id); + Board b = boardRepository.findBoardById(boardColumnRepository.findById(c.getColumnId()).getBoardId()); + return ImmutablePair.of(b, c); + } + + // TODO CLEANUP + private String handleContent(Event e) { + if (e.getDataId() == null) { + return null; + } + + switch (e.getEvent()) { + case COMMENT_CREATE: + return extractFirstContent(e, CardType.COMMENT_HISTORY); + case DESCRIPTION_CREATE: + return extractFirstContent(e, CardType.DESCRIPTION_HISTORY); + case DESCRIPTION_UPDATE: + case COMMENT_UPDATE: + List nextEvent = eventRepository.findNextEventFor(e); + return cardDataRepository.getDataLightById( + nextEvent.isEmpty() ? e.getDataId() : nextEvent.get(0).getPreviousDataId()).getContent(); + case ACTION_ITEM_CREATE: + case ACTION_LIST_CREATE: + return cardDataRepository.getDataLightById(e.getDataId()).getContent(); + case FILE_UPLOAD: + case FILE_DELETE: + return cardDataRepository.getDataLightById(e.getDataId()).getContent(); + default: + return null; + } + + } + + private String extractFirstContent(Event e, CardType type) { + CardData cd = cardDataRepository.getDataLightById(e.getDataId()); + List history = cardDataRepository.findAllDataLightByReferenceIdAndType(cd.getId(), type); + return (history.isEmpty() ? cd : history.get(0)).getContent(); + } + + private static void writeEntry(String entryName, Object toSerialize, ZipOutputStream zf, OutputStreamWriter osw) { + try { + zf.putNextEntry(new ZipEntry(entryName)); + Json.GSON.toJson(toSerialize, osw); + osw.flush(); + zf.flush(); + zf.closeEntry(); + } catch (IOException ioe) { + throw new IllegalStateException("error while serializing entry " + entryName, ioe); + } + } + + private void exportProject(ZipOutputStream zf, OutputStreamWriter osw, Project p) { + + String projectNameDir = "projects/" + p.getShortName(); + writeEntry(projectNameDir + ".json", p, zf, osw); + writeEntry(projectNameDir + "/permissions.json", + permissionService.findAllRolesAndRelatedPermissionWithUsersInProjectId(p.getId()), zf, osw); + + List>> labels = new ArrayList<>(); + for (CardLabel cl : cardLabelRepository.findLabelsByProject(p.getId())) { + labels.add(Pair.of(cl, cardLabelRepository.findListValuesByLabelId(cl.getId()))); + } + writeEntry(projectNameDir + "/labels.json", labels, zf, osw); + writeEntry(projectNameDir + "/column-definitions.json", + projectService.findMappedColumnDefinitionsByProjectId(p.getId()), zf, osw); + + for (BoardInfo boardInfo : boardRepository.findBoardInfo(p.getId())) { + exportBoard(boardInfo, p, zf, osw); + } + } + + private void exportBoard(BoardInfo boardInfo, Project p, ZipOutputStream zf, OutputStreamWriter osw) { + String boardNameDir = "boards/" + boardInfo.getShortName(); + writeEntry(boardNameDir + ".json", Pair.of(p.getShortName(), boardInfo), zf, osw); + int boardId = boardRepository.findBoardIdByShortName(boardInfo.getShortName()); + + writeEntry(boardNameDir + "/columns.json", boardColumnRepository.findAllColumnsFor(boardId), zf, osw); + writeEntry(boardNameDir + "/cards.json", cardRepository.findAllByBoardShortName(boardInfo.getShortName()), zf, + osw); + writeEntry(boardNameDir + "/statistics.json", statisticsQuery.findForBoard(boardId), zf, osw); + } +} diff --git a/src/main/java/io/lavagna/service/LavagnaImporter.java b/src/main/java/io/lavagna/service/LavagnaImporter.java new file mode 100644 index 000000000..18ebc29b4 --- /dev/null +++ b/src/main/java/io/lavagna/service/LavagnaImporter.java @@ -0,0 +1,376 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static io.lavagna.common.Read.readMatchingObjects; +import static io.lavagna.common.Read.readObject; +import static java.util.Collections.singletonList; +import io.lavagna.model.Board; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.BoardInfo; +import io.lavagna.model.CardDataIdAndOrder; +import io.lavagna.model.CardFull; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.ConfigurationKeyValue; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.Pair; +import io.lavagna.model.Permission; +import io.lavagna.model.Project; +import io.lavagna.model.Role; +import io.lavagna.model.RoleAndPermission; +import io.lavagna.model.StatisticForExport; +import io.lavagna.model.User; +import io.lavagna.model.UserIdentifier; +import io.lavagna.query.StatisticsQuery; +import io.lavagna.service.PermissionService.RoleAndPermissionsWithUsers; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.reflect.TypeToken; + +@Component +class LavagnaImporter { + + private final ConfigurationRepository configurationRepository; + private final UserRepository userRepository; + private final PermissionService permissionService; + private final ProjectService projectService; + private final BoardRepository boardRepository; + private final BoardColumnRepository boardColumnRepository; + private final CardLabelRepository cardLabelRepository; + private final CardRepository cardRepository; + private final CardDataRepository cardDataRepository; + private final StatisticsQuery statisticsQuery; + + private final ImportEvent importEvent; + + @Autowired + public LavagnaImporter(ConfigurationRepository configurationRepository, UserRepository userRepository, + PermissionService permissionService, ProjectService projectService, BoardRepository boardRepository, + BoardColumnRepository boardColumnRepository, CardLabelRepository cardLabelRepository, + CardDataRepository cardDataRepository, CardRepository cardRepository, ImportEvent importEvent, + StatisticsQuery statisticsQuery) { + this.configurationRepository = configurationRepository; + this.userRepository = userRepository; + this.permissionService = permissionService; + this.projectService = projectService; + this.boardRepository = boardRepository; + this.boardColumnRepository = boardColumnRepository; + this.cardLabelRepository = cardLabelRepository; + this.importEvent = importEvent; + this.cardRepository = cardRepository; + this.cardDataRepository = cardDataRepository; + this.statisticsQuery = statisticsQuery; + } + + public void importData(boolean overrideConfiguration, Path tempFile) { + + importConfiguration(overrideConfiguration, tempFile); + + importMissingUsers(tempFile); + importBasePermissions(tempFile); + + ImportContext context = new ImportContext(); + + importProjects(tempFile, context); + + importBoards(tempFile, context); + + // + + int eventPages = readObject("events-page-count.json", tempFile, new TypeToken() { + }); + + for (int i = 0; i < eventPages; i++) { + processEvents(readObject("events-" + i + ".json", tempFile, new TypeToken>() { + }), context, tempFile); + } + + orderAll(tempFile, context); + } + + private void orderAll(Path tempFile, ImportContext context) { + + for (String shortName : context.getImportedBoard()) { + orderCards(readObject("boards/" + shortName + "/cards.json", tempFile, new TypeToken>() { + })); + } + + for (CardDataIdAndOrder idOrder : readObject("card-data-types-order.json", tempFile, + new TypeToken>() { + })) { + + int oldId = idOrder.getFirst(); + int order = idOrder.getSecond(); + + if (context.getActionItemId().containsKey(oldId)) { + cardDataRepository.updateOrderById(context.getActionItemId().get(oldId), order); + } else if (context.getActionListId().containsKey(oldId)) { + cardDataRepository.updateOrderById(context.getActionListId().get(oldId), order); + } + } + } + + private void orderCards(List cards) { + for (CardFull cf : cards) { + int cardId = cardRepository.findCardIdByBoardNameAndSeq(cf.getBoardShortName(), cf.getSequence()); + cardRepository.updateCardOrder(cardId, cf.getOrder()); + } + } + + private void importConfiguration(boolean overrideConfiguration, Path tempFile) { + if (overrideConfiguration) { + configurationRepository.updateOrCreate(readObject("config.json", tempFile, + new TypeToken>() { + })); + } + } + + private void importBoards(Path tempFile, ImportContext context) { + for (Pair p : readMatchingObjects("boards/[^/]+\\.json", tempFile, + new TypeToken>() { + })) { + String projectShortName = p.getFirst(); + BoardInfo boardInfo = p.getSecond(); + if (context.getImportedProject().contains(projectShortName)) { + Project project = projectService.findByShortName(projectShortName); + if (!boardRepository.existsWithShortName(boardInfo.getShortName())) { + importMissingBoard(project, boardInfo, tempFile, context); + context.getImportedBoard().add(boardInfo.getShortName()); + } + } + } + } + + private void importProjects(Path tempFile, ImportContext context) { + for (Project project : readMatchingObjects("projects/[^/]+\\.json", tempFile, new TypeToken() { + })) { + if (importProject(project, tempFile)) { + context.getImportedProject().add(project.getShortName()); + } + } + } + + private void processEvents(List events, ImportContext idMapping, Path tempFile) { + for (EventFull e : events) { + processEvent(idMapping, e, tempFile); + } + } + + private void processEvent(ImportContext context, EventFull e, Path tempFile) { + + if (!context.getImportedBoard().contains(e.getBoardShortName())) { + return; + } + + importEvent.processEvent(e, context, tempFile); + } + + private void importBasePermissions(Path tempFile) { + + // add missing base permissions + Map permissions = readObject("permissions.json", tempFile, + new TypeToken>() { + }); + + permissionService.createMissingRolesWithPermissions(from(permissions)); + + // add users to roles + for (RoleAndPermissionsWithUsers v : permissions.values()) { + List assignedUsers = v.getAssignedUsers(); + Role role = new Role(v.getName()); + permissionService.assignRoleToUsers(role, removeUserWithRole(role, assignedUsers)); + } + // + } + + private Set removeUserWithRole(Role role, List users) { + Set userIds = new HashSet<>(); + for (UserIdentifier ui : users) { + User u = userRepository.findUserByName(ui.getProvider(), ui.getUsername()); + if (!permissionService.findBaseRoleAndPermissionByUserId(u.getId()).containsKey(role.getName())) { + userIds.add(u.getId()); + } + } + return userIds; + } + + private Map> from(Map from) { + Map> res = new HashMap<>(); + for (Entry kv : from.entrySet()) { + + RoleAndPermissionsWithUsers rpu = kv.getValue(); + RoleAndPermission rap = new RoleAndPermission(rpu.getName(), rpu.isRemovable(), rpu.isHidden(), + rpu.isReadOnly(), null); + if (!res.containsKey(rap)) { + res.put(rap, EnumSet.noneOf(Permission.class)); + } + for (RoleAndPermission rp : kv.getValue().getRoleAndPermissions()) { + res.get(rap).add(rp.getPermission()); + } + } + return res; + } + + /** + * Import only the users that are not present in the system. + */ + private void importMissingUsers(Path tempFile) { + List users = readObject("users.json", tempFile, new TypeToken>() { + }); + + SortedSet usersToImport = new TreeSet<>(new Comparator() { + @Override + public int compare(User o1, User o2) { + return new CompareToBuilder().append(o1.getProvider(), o2.getProvider()) + .append(o1.getUsername(), o2.getUsername()).toComparison(); + } + }); + + usersToImport.addAll(users); + usersToImport.removeAll(userRepository.findAll()); + userRepository.createUsers(usersToImport); + } + + private boolean importProject(Project project, Path tempFile) { + boolean created = projectService.createMissing(singletonList(project)).getRight().isEmpty(); + if (created) { + Project createdProject = projectService.findByShortName(project.getShortName()); + + String projectNameDir = "projects/" + project.getShortName(); + + importColumnDefinitionColor(tempFile, createdProject, projectNameDir); + importLabels(tempFile, createdProject, projectNameDir); + importProjectPermissions(tempFile, createdProject, projectNameDir); + + return true; + } else { + return false; + } + } + + private void importProjectPermissions(Path tempFile, Project createdProject, String projectNameDir) { + Map permissions = readObject(projectNameDir + "/permissions.json", + tempFile, new TypeToken>() { + }); + permissionService.createMissingRolesWithPermissionForProject(createdProject.getId(), from(permissions)); + // add users to roles + for (RoleAndPermissionsWithUsers v : permissions.values()) { + List assignedUsers = v.getAssignedUsers(); + Role role = new Role(v.getName()); + permissionService.assignRoleToUsersInProjectId(role, toUserIds(assignedUsers), createdProject.getId()); + } + } + + private Set toUserIds(List users) { + Set userIds = new HashSet<>(); + for (UserIdentifier ui : users) { + userIds.add(userRepository.findUserByName(ui.getProvider(), ui.getUsername()).getId()); + } + return userIds; + } + + private void importLabels(Path tempFile, Project createdProject, String projectNameDir) { + List>> labels = readObject(projectNameDir + "/labels.json", tempFile, + new TypeToken>>>() { + }); + + for (Pair> pLabel : labels) { + CardLabel label = pLabel.getFirst(); + + if (label.getDomain() == LabelDomain.USER) { + cardLabelRepository.addLabel(createdProject.getId(), label.isUnique(), label.getType(), + label.getDomain(), label.getName(), label.getColor()); + } + + // import label list value + if (label.getType() == LabelType.LIST && !pLabel.getSecond().isEmpty()) { + CardLabel importedCl = cardLabelRepository.findLabelByName(createdProject.getId(), label.getName(), + label.getDomain()); + for (LabelListValue llv : pLabel.getSecond()) { + cardLabelRepository.addLabelListValue(importedCl.getId(), llv.getValue()); + } + } + } + } + + private void importColumnDefinitionColor(Path tempFile, Project createdProject, String projectNameDir) { + Map importedColDef = readObject(projectNameDir + + "/column-definitions.json", tempFile, new TypeToken>() { + }); + Map currentColDef = projectService + .findMappedColumnDefinitionsByProjectId(createdProject.getId()); + + for (Entry kv : currentColDef.entrySet()) { + projectService.updateColumnDefinition(createdProject.getId(), kv.getValue().getId(), + importedColDef.get(kv.getKey()).getColor()); + } + } + + private void importMissingBoard(Project project, BoardInfo boardInfo, Path tempFile, ImportContext idMapping) { + Board createdBoard = boardRepository.createEmptyBoard(boardInfo.getName(), boardInfo.getShortName(), boardInfo.getDescription(), + project.getId()); + boardRepository.updateBoard(createdBoard.getId(), createdBoard.getName(), createdBoard.getDescription(), boardInfo.isArchived()); + List boardColumns = readObject("boards/" + boardInfo.getShortName() + "/columns.json", tempFile, + new TypeToken>() { + }); + int boardId = boardRepository.findBoardIdByShortName(boardInfo.getShortName()); + + Map colsDef = projectService + .findMappedColumnDefinitionsByProjectId(project.getId()); + + for (BoardColumn bc : boardColumns) { + BoardColumn added = boardColumnRepository.addColumnToBoard(bc.getName(), colsDef.get(bc.getStatus()) + .getId(), bc.getLocation(), boardId); + boardColumnRepository.updateOrder(added.getId(), bc.getOrder()); + + // save the old column to new column id mapping. + idMapping.getColumns().put(bc.getId(), added.getId()); + } + + List stats = readObject("boards/" + boardInfo.getShortName() + "/statistics.json", + tempFile, new TypeToken>() { + }); + + // TODO: not optimal in term of performance, use a bulk insert + for (StatisticForExport stat : stats) { + statisticsQuery.addFromImport(stat.getDate(), boardId, colsDef.get(stat.getColumnDefinition()).getId(), + stat.getLocation().toString(), stat.getCount()); + } + } + +} diff --git a/src/main/java/io/lavagna/service/Ldap.java b/src/main/java/io/lavagna/service/Ldap.java new file mode 100644 index 000000000..1acea15c8 --- /dev/null +++ b/src/main/java/io/lavagna/service/Ldap.java @@ -0,0 +1,162 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static java.lang.String.format; +import static java.util.EnumSet.of; +import static java.util.Objects.requireNonNull; +import io.lavagna.model.Key; +import io.lavagna.model.Pair; +import io.lavagna.service.LdapConnection.InitialDirContextCloseable; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class Ldap { + + private static final Logger LOG = LogManager.getLogger(); + + private final ConfigurationRepository configurationRepository; + private final LdapConnection ldapConnection; + + @Autowired + public Ldap(ConfigurationRepository configurationRepository, LdapConnection ldapConnection) { + this.configurationRepository = configurationRepository; + this.ldapConnection = ldapConnection; + } + + public boolean authenticate(String username, String password) { + + Map conf = configurationRepository + .findConfigurationFor(of(Key.LDAP_SERVER_URL, Key.LDAP_MANAGER_DN, Key.LDAP_MANAGER_PASSWORD, + Key.LDAP_USER_SEARCH_BASE, Key.LDAP_USER_SEARCH_FILTER)); + + String providerUrl = requireNonNull(conf.get(Key.LDAP_SERVER_URL)); + String ldapManagerDn = requireNonNull(conf.get(Key.LDAP_MANAGER_DN)); + String ldapManagerPwd = requireNonNull(conf.get(Key.LDAP_MANAGER_PASSWORD)); + String base = requireNonNull(conf.get(Key.LDAP_USER_SEARCH_BASE)); + String filter = requireNonNull(conf.get(Key.LDAP_USER_SEARCH_FILTER)); + // + + return authenticateWithParams(providerUrl, ldapManagerDn, ldapManagerPwd, base, filter, username, password) + .getFirst(); + } + + public Pair> authenticateWithParams(String providerUrl, String ldapManagerDn, + String ldapManagerPwd, String base, String filter, String username, String password) { + requireNonNull(username); + requireNonNull(password); + List msgs = new ArrayList<>(); + + msgs.add(format("connecting to %s with managerDn %s", providerUrl, ldapManagerDn)); + try (InitialDirContextCloseable dctx = ldapConnection.context(providerUrl, ldapManagerDn, ldapManagerPwd)) { + msgs.add(format("connected [ok]")); + msgs.add(format("now searching user \"%s\" with base %s and filter %s", username, base, filter)); + + SearchControls sc = new SearchControls(); + sc.setReturningAttributes(null); + sc.setSearchScope(SearchControls.SUBTREE_SCOPE); + + List srs = Ldap.search(dctx, base, + new MessageFormat(filter).format(new Object[] { Ldap.escapeLDAPSearchFilter(username) }), sc); + if (srs.size() != 1) { + String msg = format("error for username \"%s\" we have %d results instead of 1 [error]", username, + srs.size()); + msgs.add(msg); + LOG.info(msg, username, srs.size()); + return Pair.of(false, msgs); + } + + msgs.add("user found, now will connect with given password [ok]"); + + SearchResult sr = srs.get(0); + + try (InitialDirContextCloseable uctx = ldapConnection.context(providerUrl, sr.getNameInNamespace(), + password)) { + msgs.add("user authenticated, everything seems ok [ok]"); + return Pair.of(true, msgs); + } catch (NamingException e) { + String msg = format("error while checking with username \"%s\" with message: %s [error]", username, + e.getMessage()); + msgs.add(msg); + LOG.info(msg, e); + return Pair.of(false, msgs); + } + } catch (Throwable e) { + String errMsg = format( + "error while opening the connection with message: %s [error], check the logs for a more complete trace", + e.getMessage()); + msgs.add(errMsg); + LOG.error(errMsg, e); + return Pair.of(false, msgs); + } + } + + private static List search(DirContext dctx, String base, String filter, SearchControls sc) + throws NamingException { + List res = new ArrayList<>(); + NamingEnumeration search = dctx.search(base, filter, sc); + while (search.hasMore()) { + res.add(search.next()); + } + return res; + } + + // imported from + // https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java . + // Checked against spring implementation too... + private static final String escapeLDAPSearchFilter(String filter) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < filter.length(); i++) { + char curChar = filter.charAt(i); + switch (curChar) { + case '\\': + sb.append("\\5c"); + break; + case '*': + sb.append("\\2a"); + break; + case '(': + sb.append("\\28"); + break; + case ')': + sb.append("\\29"); + break; + case '\u0000': + sb.append("\\00"); + break; + default: + sb.append(curChar); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/service/LdapConnection.java b/src/main/java/io/lavagna/service/LdapConnection.java new file mode 100644 index 000000000..26bd82546 --- /dev/null +++ b/src/main/java/io/lavagna/service/LdapConnection.java @@ -0,0 +1,45 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; + +import org.springframework.stereotype.Service; + +@Service +public class LdapConnection { + + static class InitialDirContextCloseable extends InitialDirContext implements AutoCloseable { + + private InitialDirContextCloseable(Properties env) throws NamingException { + super(env); + } + } + + InitialDirContextCloseable context(String providerUrl, String principal, String password) throws NamingException { + Properties env = new Properties(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, providerUrl); + env.put(Context.SECURITY_PRINCIPAL, principal); + env.put(Context.SECURITY_CREDENTIALS, password); + return new InitialDirContextCloseable(env); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/service/MySqlFullTextSupportService.java b/src/main/java/io/lavagna/service/MySqlFullTextSupportService.java new file mode 100644 index 000000000..44a21bede --- /dev/null +++ b/src/main/java/io/lavagna/service/MySqlFullTextSupportService.java @@ -0,0 +1,66 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.query.MySqlFullTextSupportQuery; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service for handling the asynchronous copy of text data from INNODB tables to MYSAM ones. + *

+ * As we support older version of MySQL, the fulltext search engine is not present in the INNODB tables. + */ +@Service +@Transactional(propagation = Propagation.NESTED) +public class MySqlFullTextSupportService { + + private static final Logger LOG = LogManager.getLogger(); + + private final MySqlFullTextSupportQuery queries; + + @Autowired + public MySqlFullTextSupportService(MySqlFullTextSupportQuery queries) { + this.queries = queries; + } + + public void syncNewCards() { + int rowAffected = queries.syncNewCards(); + LOG.debug("syncNewCards : updated {} row", rowAffected); + } + + public void syncUpdatedCards() { + int rowAffected = queries.syncUpdatedCards(); + LOG.debug("syncUpdatedCards : updated {} row", rowAffected); + } + + public void syncNewCardData() { + int rowAffected = queries.syncNewCardData(); + LOG.debug("syncNewCardData : updated {} row", rowAffected); + } + + public void syncUpdatedCardData() { + int rowAffected = queries.syncUpdatedCardData(); + LOG.debug("syncUpdatedCardData : updated {} row", rowAffected); + } + +} diff --git a/src/main/java/io/lavagna/service/NotificationService.java b/src/main/java/io/lavagna/service/NotificationService.java new file mode 100644 index 000000000..3aabec615 --- /dev/null +++ b/src/main/java/io/lavagna/service/NotificationService.java @@ -0,0 +1,279 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.CardFull; +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.Key; +import io.lavagna.model.MailConfig; +import io.lavagna.model.User; +import io.lavagna.query.NotificationQuery; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.ImmutableTriple; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.mail.MailException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.MustacheException; + +/** + * Handle the whole email notification process. + */ +@Service +@Transactional(readOnly = false) +public class NotificationService { + + private static final Logger LOG = LogManager.getLogger(); + + private final ConfigurationRepository configurationRepository; + private final BoardColumnRepository boardColumnRepository; + private final CardDataRepository cardDataRepository; + private final CardRepository cardRepository; + private final UserRepository userRepository; + + private final MessageSource messageSource; + + private final NamedParameterJdbcTemplate jdbc; + private final NotificationQuery queries; + + @Autowired + public NotificationService(ConfigurationRepository configurationRepository, UserRepository userRepository, + CardDataRepository cardDataRepository, CardRepository cardRepository, + BoardColumnRepository boardColumnRepository, MessageSource messageSource, NamedParameterJdbcTemplate jdbc, + NotificationQuery queries) { + this.configurationRepository = configurationRepository; + this.userRepository = userRepository; + this.cardDataRepository = cardDataRepository; + this.cardRepository = cardRepository; + this.boardColumnRepository = boardColumnRepository; + this.messageSource = messageSource; + this.jdbc = jdbc; + this.queries = queries; + } + + /** + * Return a list of user id to notify. + * + * @param upTo + * @return + */ + public Set check(Date upTo) { + + final List userWithChanges = new ArrayList<>(); + List res = jdbc.query(queries.countNewForUsersId(), new RowMapper() { + + @Override + public SqlParameterSource mapRow(ResultSet rs, int rowNum) throws SQLException { + int userId = rs.getInt("USER_ID"); + userWithChanges.add(userId); + return new MapSqlParameterSource("count", rs.getInt("COUNT_EVENT_ID")).addValue("userId", userId); + } + }); + + if (!res.isEmpty()) { + jdbc.batchUpdate(queries.updateCount(), res.toArray(new SqlParameterSource[res.size()])); + } + queries.updateCheckDate(upTo); + + // select users that have pending notifications that were not present in this check round + MapSqlParameterSource userWithChangesParam = new MapSqlParameterSource("userWithChanges", userWithChanges); + // + List usersToNotify = jdbc.queryForList(queries.usersToNotify() + " " + + (userWithChanges.isEmpty() ? "" : queries.notIn()), userWithChangesParam, Integer.class); + // + jdbc.update(queries.reset() + " " + (userWithChanges.isEmpty() ? "" : queries.notIn()), userWithChangesParam); + // + return new TreeSet<>(usersToNotify); + } + + /** + * Filter events that are older that the "WATCHED_BY" and "ASSIGNED" creation label event if the lastSent is null. + */ + private static List filterEvents(List evs, int userId, Date lastSent) { + if (lastSent != null) { + return evs; + } + List res = new ArrayList<>(); + + Set cardIdsToToKeep = new HashSet<>(); + + for (Event e : evs) { + if (cardIdsToToKeep.contains(e.getCardId()) + || (("ASSIGNED".equals(e.getLabelName()) || "WATCHED_BY".equals(e.getLabelName())) + && e.getEvent() == EventType.LABEL_CREATE && e.getLabelType() == LabelType.USER && e + .getUserId() == userId)) { + res.add(e); + cardIdsToToKeep.add(e.getCardId()); + } + } + + return res; + } + + private List composeCardSection(int cardId, List events, EventsContext context) { + // + + List res = new ArrayList<>(); + for (Event e : events) { + if (EnumUtils.isValidEnum(SupportedEventType.class, e.getEvent().toString())) { + ImmutablePair message = SupportedEventType.valueOf(e.getEvent().toString()).toKeyAndParam(e, + context, cardDataRepository); + res.add(messageSource.getMessage(message.getKey(), message.getValue(), Locale.ENGLISH)); + } + } + return res; + } + + private ImmutableTriple composeEmailForUser(User user, EventsContext context) + throws MustacheException, IOException { + + List> cardsModel = new ArrayList<>(); + + StringBuilder subject = new StringBuilder(); + for (Entry> kv : context.events.entrySet()) { + + Map cardModel = new HashMap<>(); + + CardFull cf = context.cards.get(kv.getKey()); + StringBuilder cardName = new StringBuilder(cf.getBoardShortName()).append("-").append(cf.getSequence()) + .append(" ").append(cf.getName()); + + cardModel.put("cardFull", cf); + cardModel.put("cardName", cardName.toString()); + cardModel.put("cardEvents", composeCardSection(kv.getKey(), kv.getValue(), context)); + + subject.append(cf.getBoardShortName()).append("-").append(cf.getSequence()).append(", "); + + cardsModel.add(cardModel); + } + + com.samskivert.mustache.Mustache.Compiler compiler = Mustache.compiler().escapeHTML(true).defaultValue(""); + + Map tmplModel = new HashMap<>(); + tmplModel.put("cards", cardsModel); + tmplModel.put("baseApplicationUrl", + StringUtils.appendIfMissing(configurationRepository.getValue(Key.BASE_APPLICATION_URL), "/")); + + String text = compiler.compile( + new InputStreamReader(new ClassPathResource("/io/lavagna/notification/email.txt").getInputStream(), + StandardCharsets.UTF_8)).execute(tmplModel); + + String html = compiler.compile( + new InputStreamReader(new ClassPathResource("/io/lavagna/notification/email.html").getInputStream(), + StandardCharsets.UTF_8)).execute(tmplModel); + + return ImmutableTriple.of(subject.substring(0, subject.length() - ", ".length()), text, html); + } + + /** + * Send email (if all the conditions are met) to the user. + * + * @param userId + * @param upTo + * @param emailEnabled + * @param mailConfig + */ + public void notifyUser(int userId, Date upTo, boolean emailEnabled, MailConfig mailConfig) { + Date lastSent = queries.lastEmailSent(userId); + List events = filterEvents( + queries.eventsForUser(userId, ObjectUtils.firstNonNull(lastSent, DateUtils.addDays(upTo, -1)), upTo), + userId, lastSent); + + User user = userRepository.findById(userId); + if (!events.isEmpty() && mailConfig != null && mailConfig.isMinimalConfigurationPresent() && emailEnabled + && user.canSendEmail()) { + try { + sendEmailToUser(user, events, mailConfig); + } catch (MustacheException | IOException | MailException e) { + LOG.warn("Error while sending an email to user with id " + user.getId(), e); + } + } + + // + queries.updateSentEmailDate(upTo, userId); + } + + private void sendEmailToUser(User user, List events, MailConfig mailConfig) throws MustacheException, + IOException { + + Set userIds = new HashSet<>(); + userIds.add(user.getId()); + Set cardIds = new HashSet<>(); + Set cardDataIds = new HashSet<>(); + Set columnIds = new HashSet<>(); + + for (Event e : events) { + cardIds.add(e.getCardId()); + userIds.add(e.getUserId()); + + addIfNotNull(userIds, e.getValueUser()); + addIfNotNull(cardIds, e.getValueCard()); + + addIfNotNull(cardDataIds, e.getDataId()); + addIfNotNull(cardDataIds, e.getPreviousDataId()); + + addIfNotNull(columnIds, e.getColumnId()); + addIfNotNull(columnIds, e.getPreviousColumnId()); + } + + final ImmutableTriple subjectAndText = composeEmailForUser(user, + new EventsContext(events, userRepository.findByIds(userIds), cardRepository.findAllByIds(cardIds), + cardDataRepository.findDataByIds(cardDataIds), boardColumnRepository.findByIds(columnIds))); + + mailConfig.send(user.getEmail(), StringUtils.substring("Lavagna: " + subjectAndText.getLeft(), 0, 78), + subjectAndText.getMiddle(), subjectAndText.getRight()); + } + + private static void addIfNotNull(Set s, T v) { + if (v != null) { + s.add(v); + } + } +} diff --git a/src/main/java/io/lavagna/service/PermissionService.java b/src/main/java/io/lavagna/service/PermissionService.java new file mode 100644 index 000000000..ae7de8b9d --- /dev/null +++ b/src/main/java/io/lavagna/service/PermissionService.java @@ -0,0 +1,399 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.Permission; +import io.lavagna.model.ProjectRoleAndPermission; +import io.lavagna.model.Role; +import io.lavagna.model.RoleAndMetadata; +import io.lavagna.model.RoleAndPermission; +import io.lavagna.model.User; +import io.lavagna.model.UserIdentifier; +import io.lavagna.query.PermissionQuery; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class PermissionService { + + private final NamedParameterJdbcTemplate jdbc; + private final PermissionQuery queries; + private final UserRepository userRepository; + + @Autowired + public PermissionService(NamedParameterJdbcTemplate jdbc, PermissionQuery queries, UserRepository userRepository) { + this.jdbc = jdbc; + this.queries = queries; + this.userRepository = userRepository; + } + + /** + * A role can be without any permission. + */ + public Map findBaseRoleAndPermissionByUserId(int userId) { + return toMap(queries.findBaseRoleAndPermissionByUserId(userId)); + } + + public Map findRoleAndPermissionByUserIdInProjectId(int userId, int projectId) { + return toMap(queries.findRoleAndPermissionByUserIdInProjectId(userId, projectId)); + } + + public Set findBasePermissionByUserId(int userId) { + return toSet(queries.findBaseRoleAndPermissionByUserId(userId)); + } + + public RoleAndMetadata findRoleByName(String name) { + return queries.findRoleByName(name); + } + + public RoleAndMetadata findRoleInProjectByName(int projectId, String name) { + return queries.findRoleInProjectIdByName(projectId, name); + } + + public ProjectRoleAndPermissionFullHolder findPermissionsGroupedByProjectForUserId(int userId) { + List found = queries.findPermissionsGroupedByProjectForUserId(userId); + Map> res = new HashMap<>(); + Map> resById = new HashMap<>(); + for (ProjectRoleAndPermission p : found) { + if (p.getPermission() == null) { + continue; + } + + if (!res.containsKey(p.getProjectShortName())) { + res.put(p.getProjectShortName(), EnumSet.noneOf(Permission.class)); + } + res.get(p.getProjectShortName()).add(p.getPermission()); + + if (!resById.containsKey(p.getProjectId())) { + resById.put(p.getProjectId(), EnumSet.noneOf(Permission.class)); + } + resById.get(p.getProjectId()).add(p.getPermission()); + } + return new ProjectRoleAndPermissionFullHolder(res, resById); + } + + public Set findPermissionByUsernameInProjectId(int userId, int projectId) { + return toSet(queries.findRoleAndPermissionByUserIdInProjectId(userId, projectId)); + } + + public Map findAllRolesAndRelatedPermission() { + return toMap(queries.findAllRolesAndRelatedPermission()); + } + + public Map findAllRolesAndRelatedPermissionWithUsers() { + Map res = new TreeMap<>(); + for (RoleAndPermission rap : queries.findAllRolesAndRelatedPermission()) { + if (!res.containsKey(rap.getRoleName())) { + res.put(rap.getRoleName(), + new RoleAndPermissionsWithUsers(rap, queries.findUserIdentifierByRole(rap.getRoleName()))); + } + if (rap.getPermission() != null) { + res.get(rap.getRoleName()).getRoleAndPermissions().add(rap); + } + } + return res; + } + + public Map findAllRolesAndRelatedPermissionWithUsersInProjectId(int projectId) { + Map res = new TreeMap<>(); + for (RoleAndPermission rap : queries.findAllRolesAndRelatedPermissionInProjectId(projectId)) { + if (!res.containsKey(rap.getRoleName())) { + res.put(rap.getRoleName(), + new RoleAndPermissionsWithUsers(rap, queries.findUserIdentifierByRoleAndProjectId( + rap.getRoleName(), projectId))); + } + if (rap.getPermission() != null) { + res.get(rap.getRoleName()).getRoleAndPermissions().add(rap); + } + } + return res; + } + + public Map findAllRolesAndRelatedPermissionInProjectId(int projectId) { + return toMap(queries.findAllRolesAndRelatedPermissionInProjectId(projectId)); + } + + /** + * Return 1 if created + * + * @param role + * @return + */ + @Transactional(readOnly = false) + public int createRole(Role role) { + return queries.createRole(Objects.requireNonNull(role).getName()); + } + + @Transactional(readOnly = false) + public int createRoleInProjectId(Role role, int projectId) { + return queries.createRoleInProjectId(Objects.requireNonNull(role).getName(), projectId); + } + + @Transactional(readOnly = false) + public int createFullRoleInProjectId(Role role, int projectId, boolean removable, boolean hidden, boolean readOnly) { + return queries.createFullRoleInProjectId(Objects.requireNonNull(role).getName(), projectId, removable, hidden, + readOnly); + } + + @Transactional(readOnly = false) + public void createMissingRolesWithPermissions(Map> rolesWithPermissions) { + + Set currentRoles = findAllRolesAndRelatedPermission().keySet(); + + for (Entry> kv : rolesWithPermissions.entrySet()) { + RoleAndPermission rp = kv.getKey(); + if (!currentRoles.contains(rp.getRoleName())) { + queries.createFullRole(rp.getRoleName(), rp.isRemovable(), rp.isHidden(), rp.isHidden()); + } + updatePermissionsToRole(new Role(rp.getRoleName()), kv.getValue()); + } + } + + @Transactional(readOnly = false) + public void createMissingRolesWithPermissionForProject(int projectId, Map> p) { + createMissingRolesWithPermissionForProjects(Collections.singletonMap(projectId, p)); + } + + @Transactional(readOnly = false) + public void createMissingRolesWithPermissionForProjects(Map>> r) { + for (Entry>> projIdToRolesAndPermissions : r.entrySet()) { + + int projectId = projIdToRolesAndPermissions.getKey(); + + Set currentRoles = findAllRolesAndRelatedPermissionInProjectId(projectId).keySet(); + + for (Entry> kv : projIdToRolesAndPermissions.getValue().entrySet()) { + RoleAndPermission rp = kv.getKey(); + if (!currentRoles.contains(rp.getRoleName())) { + createFullRoleInProjectId(new Role(rp.getRoleName()), projectId, rp.isRemovable(), rp.isHidden(), + rp.isReadOnly()); + } + updatePermissionsToRoleInProjectId(new Role(rp.getRoleName()), kv.getValue(), projectId); + } + } + } + + /** + * Return 1 if deleted + * + * @param role + * @return + */ + @Transactional(readOnly = false) + public int deleteRole(Role role) { + Objects.requireNonNull(role); + return queries.deleteRole(role.getName()); + } + + @Transactional(readOnly = false) + public int deleteRoleInProjectId(Role role, int projectId) { + Objects.requireNonNull(role); + return queries.deleteRoleInProjectId(role.getName(), projectId); + } + + @Transactional(readOnly = false) + public void updatePermissionsToRole(Role role, Set enabledPermissions) { + Objects.requireNonNull(role); + Objects.requireNonNull(enabledPermissions); + + // step 1: remove all permissions + queries.deletePermissions(role.getName()); + // step 2: add the enabled permission + jdbc.batchUpdate(queries.addPermission(), from(role, enabledPermissions)); + } + + @Transactional(readOnly = false) + public void updatePermissionsToRoleInProjectId(Role role, Set permissions, int projectId) { + Objects.requireNonNull(role); + Objects.requireNonNull(permissions); + Permission.ensurePermissionForProject(permissions); + + // step 1: remove all permissions + queries.deletePermissionsInProjectId(role.getName(), projectId); + // step 2: add the enabled permission + jdbc.batchUpdate(queries.addPermissionInProjectId(), addProjectId(from(role, permissions), projectId)); + } + + private void checkRoleCondition(String roleName, Set usersId) { + if ("ANONYMOUS".equals(roleName) && !usersId.isEmpty()) { + Validate.isTrue(usersId.size() == 1); + Validate.isTrue(userRepository.findById(usersId.iterator().next()).isAnonymous()); + } + } + + @Transactional(readOnly = false) + public void assignRolesToUsers(Map> rolesToUsersId) { + for (Entry> roleToUsersId : rolesToUsersId.entrySet()) { + assignRoleToUsers(roleToUsersId.getKey(), roleToUsersId.getValue()); + } + } + + @Transactional(readOnly = false) + public void assignRoleToUsers(Role role, Set userIds) { + Objects.requireNonNull(role); + Objects.requireNonNull(userIds); + + checkRoleCondition(role.getName(), userIds); + + jdbc.batchUpdate(queries.assignRoleToUser(), fromUserIdAndRoleName(role, userIds)); + } + + @Transactional(readOnly = false) + public void assignRoleToUsersInProjectId(Role role, Set userIds, int projectId) { + Objects.requireNonNull(role); + Objects.requireNonNull(userIds); + + checkRoleCondition(role.getName(), userIds); + + jdbc.batchUpdate(queries.assignRoleToUsersInProjectId(), + addProjectId(fromUserIdAndRoleName(role, userIds), projectId)); + } + + @Transactional(readOnly = false) + public void removeRoleToUsers(Role role, Set userIds) { + Objects.requireNonNull(role); + Objects.requireNonNull(userIds); + + checkRoleCondition(role.getName(), userIds); + + jdbc.batchUpdate(queries.removeRoleToUsers(), fromUserIdAndRoleName(role, userIds)); + } + + @Transactional(readOnly = false) + public void removeRoleToUsersInProjectId(Role role, Set userIds, int projectId) { + Objects.requireNonNull(role); + Objects.requireNonNull(userIds); + + checkRoleCondition(role.getName(), userIds); + + jdbc.batchUpdate(queries.removeRoleToUsersInProjectId(), + addProjectId(fromUserIdAndRoleName(role, userIds), projectId)); + } + + public List findUserByRole(Role role) { + Objects.requireNonNull(role); + return queries.findUserByRole(role.getName()); + } + + public List findUserByRoleAndProjectId(Role role, int projectId) { + Objects.requireNonNull(role); + return queries.findUserByRoleAndProjectId(role.getName(), projectId); + } + + private static Map toMap(List l) { + Map res = new TreeMap<>(); + for (RoleAndPermission rap : l) { + if (!res.containsKey(rap.getRoleName())) { + res.put(rap.getRoleName(), new RoleAndPermissions(rap)); + } + if (rap.getPermission() != null) { + res.get(rap.getRoleName()).roleAndPermissions.add(rap); + } + } + return res; + } + + private static Set toSet(List rp) { + Set permissions = EnumSet.noneOf(Permission.class); + for (RoleAndPermission rap : rp) { + if (rap.getPermission() != null) { + permissions.add(rap.getPermission()); + } + } + return permissions; + } + + private static MapSqlParameterSource[] fromUserIdAndRoleName(Role role, Set userIds) { + List ret = new ArrayList<>(userIds.size()); + for (Integer userId : userIds) { + ret.add(new MapSqlParameterSource("userId", userId).addValue("roleName", role.getName())); + } + + return ret.toArray(new MapSqlParameterSource[ret.size()]); + } + + private static MapSqlParameterSource[] addProjectId(MapSqlParameterSource[] s, int projectId) { + for (MapSqlParameterSource param : s) { + param.addValue("projectId", projectId); + } + return s; + } + + private static MapSqlParameterSource[] from(Role role, Set l) { + List ret = new ArrayList<>(l.size()); + + for (Permission p : l) { + ret.add(new MapSqlParameterSource("permission", p.toString()).addValue("roleName", role.getName())); + } + + return ret.toArray(new MapSqlParameterSource[ret.size()]); + } + + @Getter + public static class RoleAndPermissions { + private final String name; + private final boolean removable; + private final boolean hidden; + private final boolean readOnly; + private final List roleAndPermissions = new ArrayList<>(); + + private RoleAndPermissions(RoleAndPermission base) { + this.name = base.getRoleName(); + this.removable = base.isRemovable(); + this.hidden = base.isHidden(); + this.readOnly = base.isReadOnly(); + } + } + + @Getter + public static class RoleAndPermissionsWithUsers extends RoleAndPermissions { + + private final List assignedUsers; + + public RoleAndPermissionsWithUsers(RoleAndPermission base, List assignedUsers) { + super(base); + this.assignedUsers = assignedUsers; + } + } + + @Getter + @AllArgsConstructor + public static class ProjectRoleAndPermissionFullHolder { + private final Map> permissionsByProject; + private final Map> permissionsByProjectId; + } +} diff --git a/src/main/java/io/lavagna/service/ProjectService.java b/src/main/java/io/lavagna/service/ProjectService.java new file mode 100644 index 000000000..a63a6df23 --- /dev/null +++ b/src/main/java/io/lavagna/service/ProjectService.java @@ -0,0 +1,231 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.StringUtils.trimToNull; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.Permission; +import io.lavagna.model.Project; +import io.lavagna.model.ProjectWithEventCounts; +import io.lavagna.model.Role; +import io.lavagna.model.User; +import io.lavagna.query.ProjectQuery; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link Project} related service. + */ +@Service +@Transactional(readOnly = true) +public class ProjectService { + + private final NamedParameterJdbcTemplate jdbc; + private final CardLabelRepository cardLabelRepository; + private final PermissionService permissionService; + private final ProjectQuery queries; + + @Autowired + public ProjectService(NamedParameterJdbcTemplate jdbc, ProjectQuery queries, + CardLabelRepository cardLabelRepository, PermissionService permissionService) { + this.jdbc = jdbc; + this.queries = queries; + this.cardLabelRepository = cardLabelRepository; + this.permissionService = permissionService; + } + + private static T firstOrNull(List t) { + return t.isEmpty() ? null : t.get(0); + } + + @Transactional(readOnly = false) + public Project create(String name, String shortName, String description) { + queries.createProject(trimToNull(name), trimToNull(shortName.toUpperCase(Locale.ENGLISH)), + trimToNull(description)); + Project project = queries.findLastCreatedProject(); + + // Add default labels to the Project + cardLabelRepository.addSystemLabels(project.getId()); + + // create default column definitions + createDefaultColumnDefinitions(project.getId()); + + // + Role anonymousRole = new Role("ANONYMOUS"); + permissionService.createFullRoleInProjectId(anonymousRole, project.getId(), false, true, true); + permissionService.updatePermissionsToRole(anonymousRole, EnumSet.of(Permission.READ)); + // + + return project; + } + + private void createDefaultColumnDefinitions(int projectId) { + List params = new ArrayList<>(ColumnDefinition.values().length); + for (ColumnDefinition definition : ColumnDefinition.values()) { + SqlParameterSource param = new MapSqlParameterSource("value", definition.toString()).addValue("color", + definition.getDefaultColor()).addValue("projectId", projectId); + params.add(param); + } + jdbc.batchUpdate(queries.createColumnDefinition(), params.toArray(new SqlParameterSource[params.size()])); + } + + @Transactional(readOnly = false) + public int updateColumnDefinition(int projectId, int columnDefinitionId, int color) { + return queries.updateColumnDefinition(color, projectId, columnDefinitionId); + } + + @Transactional(readOnly = false) + public Project updateProject(int projectId, String name, String description, boolean archived) { + queries.updateProject(projectId, name, description, archived); + return queries.findById(projectId); + } + + /** + * Bulk creation of projects. Will skip the project that already exists. + * + * @param projects + */ + @Transactional(readOnly = false) + public ImmutablePair, List> createMissing(List projects) { + List created = new ArrayList<>(); + List skipped = new ArrayList<>(); + Set usedShortNames = new HashSet<>(); + for (Project pi : findAll()) { + usedShortNames.add(pi.getShortName()); + } + + for (Project p : projects) { + if (!usedShortNames.contains(p.getShortName())) { + Project createdProject = create(p.getName(), p.getShortName(), p.getDescription()); + updateProject(createdProject.getId(), createdProject.getName(), createdProject.getDescription(), p.isArchived()); + created.add(createdProject); + } else { + skipped.add(p); + } + } + + return ImmutablePair.of(Collections.unmodifiableList(created), Collections.unmodifiableList(skipped)); + } + + public Project findById(int projectId) { + return queries.findById(projectId); + } + + public Project findByShortName(String shortName) { + return queries.findByShortName(shortName); + } + + public List findAll() { + return queries.findAll(); + } + + /** + * Find the projects that a user has a specific READ permission present as a _project_ level permission. + * + * @param user + * @return + */ + public List findAllForUserWithPermissionInProject(User user) { + return queries.findAllForUser(user.getId(), Permission.READ.toString()); + } + + // --------- + + public String findRelatedProjectShortNameByBoardShortname(String shortName) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByBoardShortname(shortName))); + } + + // FIXME: fetch directly the project short name? + private String fromProjectIdToShortName(Integer projectId) { + return projectId != null ? findById(projectId).getShortName() : null; + } + + public String findRelatedProjectShortNameByCardId(int cardId) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByCardId(cardId))); + } + + public String findRelatedProjectShortNameByEventId(int eventId) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByEventId(eventId))); + } + + public String findRelatedProjectShortNameByColumnId(int columnId) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByColumnId(columnId))); + } + + public String findRelatedProjectShortNameByCardDataId(int cardDataId) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByCardDataId(cardDataId))); + } + + public String findRelatedProjectShortNameByLabelId(Integer labelId) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByLabelId(labelId))); + } + + public String findRelatedProjectShortNameByColumnDefinitionId(int columnDefinitionId) { + return fromProjectIdToShortName(firstOrNull(queries + .findRelatedProjectIdByColumnDefinitionId(columnDefinitionId))); + } + + public String findRelatedProjectShortNameByLabelValueId(Integer labelValueId) { + return fromProjectIdToShortName(firstOrNull(queries.findRelatedProjectIdByLabelValueId(labelValueId))); + } + + public List findColumnDefinitionsByProjectId(int projectId) { + return queries.findColumnDefinitionsByProjectId(projectId); + } + + public Map findMappedColumnDefinitionsByProjectId(int projectId) { + Map mappedDefinitions = new EnumMap<>(ColumnDefinition.class); + for (BoardColumnDefinition definition : findColumnDefinitionsByProjectId(projectId)) { + mappedDefinitions.put(definition.getValue(), definition); + } + return mappedDefinitions; + } + + public boolean existsWithShortName(String shortName) { + return Integer.valueOf(1).equals(queries.existsWithShortName(shortName)); + } + + public List findProjectsActivityByUserInProjects(int userId, Collection projectIds) { + if (projectIds.isEmpty()) { + return Collections.emptyList(); + } else { + return queries.findProjectsByUserActivityInProjects(userId, projectIds); + } + } + + public List findProjectsActivityByUser(int userId) { + return queries.findProjectsByUserActivity(userId); + } +} diff --git a/src/main/java/io/lavagna/service/Scheduler.java b/src/main/java/io/lavagna/service/Scheduler.java new file mode 100644 index 000000000..876884534 --- /dev/null +++ b/src/main/java/io/lavagna/service/Scheduler.java @@ -0,0 +1,114 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.common.DatabaseMigrationDoneEvent; +import io.lavagna.common.Json; +import io.lavagna.model.Key; +import io.lavagna.model.MailConfig; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.Environment; +import org.springframework.mail.MailException; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +/** + * Simple scheduler. Note: it's not cluster aware. + */ +@Service +public class Scheduler implements ApplicationListener { + + private static final Logger LOG = LogManager.getLogger(); + + private final TaskScheduler taskScheduler; + private final Environment env; + private final ConfigurationRepository configurationRepository; + private final MySqlFullTextSupportService mySqlFullTextSupportService; + private final NotificationService notificationService; + + @Autowired + public Scheduler(TaskScheduler taskScheduler, Environment env, ConfigurationRepository configurationRepository, + MySqlFullTextSupportService mySqlFullTextSupportService, NotificationService notificationService) { + + this.taskScheduler = taskScheduler; + this.env = env; + this.configurationRepository = configurationRepository; + this.mySqlFullTextSupportService = mySqlFullTextSupportService; + this.notificationService = notificationService; + } + + private static class EmailNotificationHandler implements Runnable { + + private final ConfigurationRepository configurationRepository; + private final NotificationService notificationService; + + private EmailNotificationHandler(ConfigurationRepository configurationRepository, + NotificationService notificationService) { + this.configurationRepository = configurationRepository; + this.notificationService = notificationService; + } + + @Override + public void run() { + + Date upTo = new Date(); + Set usersToNotify = notificationService.check(upTo); + + Map conf = configurationRepository.findConfigurationFor(EnumSet.of(Key.SMTP_ENABLED, + Key.SMTP_CONFIG)); + + boolean enabled = Boolean.parseBoolean(ObjectUtils.firstNonNull(conf.get(Key.SMTP_ENABLED), "false")); + MailConfig mailConfig = Json.GSON.fromJson(conf.get(Key.SMTP_CONFIG), MailConfig.class); + for (int userId : usersToNotify) { + try { + notificationService.notifyUser(userId, upTo, enabled, mailConfig); + } catch (MailException me) { + LOG.error("Error while sending email to userId " + userId, me); + } + } + } + } + + @Override + public void onApplicationEvent(DatabaseMigrationDoneEvent event) { + if ("MYSQL".equals(env.getProperty("datasource.dialect"))) { + taskScheduler.scheduleAtFixedRate(new Runnable() { + + @Override + public void run() { + mySqlFullTextSupportService.syncNewCards(); + mySqlFullTextSupportService.syncUpdatedCards(); + mySqlFullTextSupportService.syncNewCardData(); + mySqlFullTextSupportService.syncUpdatedCardData(); + } + }, 2 * 1000); + } + + taskScheduler.scheduleAtFixedRate(new EmailNotificationHandler(configurationRepository, notificationService), + 30 * 1000); + } +} diff --git a/src/main/java/io/lavagna/service/SearchFilter.java b/src/main/java/io/lavagna/service/SearchFilter.java new file mode 100644 index 000000000..b96ab2942 --- /dev/null +++ b/src/main/java/io/lavagna/service/SearchFilter.java @@ -0,0 +1,381 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.time.DateUtils.addDays; +import static org.apache.commons.lang3.time.DateUtils.addMonths; +import static org.apache.commons.lang3.time.DateUtils.addWeeks; +import static org.apache.commons.lang3.time.DateUtils.parseDateStrictly; +import static org.apache.commons.lang3.time.DateUtils.truncate; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.UserWithPermission; +import io.lavagna.query.SearchQuery; + +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public class SearchFilter { + + private final FilterType type; + private final String name; + private final SearchFilterValue value; + + public static SearchFilter filter(FilterType type, ValueType valueType, Object value) { + return new SearchFilter(type, null, new SearchFilterValue(valueType, value)); + } + + public static SearchFilter filterByColumnDefinition(ColumnDefinition value) { + return filter(SearchFilter.FilterType.STATUS, SearchFilter.ValueType.STRING, value.toString()); + } + + public SearchFilter(FilterType type, String name, SearchFilterValue value) { + this.type = type; + this.name = name; + this.value = value; + } + + + @Getter + public static class SearchContext { + private final UserWithPermission currentUser; + private final Map userNameToId; + private final Map cardNameToId; + + public SearchContext(UserWithPermission currentUser, Map userNameToId, + final Map cardNameToId) { + this.currentUser = currentUser; + this.userNameToId = userNameToId; + this.cardNameToId = cardNameToId; + } + } + + public enum FilterType { + + USER_LABEL { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + String r = queries.findByUserLabel(); + params.add(sf.name); + if (sf.value == null) { + return r; + } + + String val = sf.value.value.toString(); + // string + params.add(val); + // int + params.add(tryParse(val)); + // timestamp from/to + addDateParams(sf, params); + // user + params.add(from(context.userNameToId, val)); + // card + params.add(from(context.cardNameToId, val)); + // list value + params.add(val); + + return r + " " + queries.andLabelValueString(); + } + + }, + ASSIGNED { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + + params.add("ASSIGNED"); + + if (sf.value.type == ValueType.UNASSIGNED) { + return queries.findCardIdNotInOpen() + " " + queries.findBySystemLabel() + " " + + queries.findCardIdNotInClose(); + } else { + addUserToParam(context.currentUser, params, context.userNameToId, sf); + return queries.findBySystemLabel() + " " + queries.andLabelValueUser(); + } + } + }, + CREATED_BY { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + addUserToParam(context.currentUser, params, context.userNameToId, sf); + return queries.findByCardCreationEventUser(); + } + }, + CREATED { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + + addDateParams(sf, params); + + return queries.findByCardCreationEventDate(); + } + }, + WATCHED_BY { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + params.add("WATCHED_BY"); + + if (sf.value.type == ValueType.UNASSIGNED) { + return queries.findCardIdNotInOpen() + " " + queries.findBySystemLabel() + " " + + queries.findCardIdNotInClose(); + } else { + addUserToParam(context.currentUser, params, context.userNameToId, sf); + return queries.findBySystemLabel() + " " + queries.andLabelValueUser(); + } + } + }, + MILESTONE { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + + params.add("MILESTONE"); + + if (sf.value.type == ValueType.UNASSIGNED) { + return queries.findCardIdNotInOpen() + " " + queries.findBySystemLabel() + " " + + queries.findCardIdNotInClose(); + } else { + params.add(sf.value.value); + return queries.findBySystemLabel() + " " + queries.andLabelListValueEq(); + } + + } + + }, + + DUE_DATE { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + + params.add("DUE_DATE"); + + addDateParams(sf, params); + + return queries.findBySystemLabel() + " " + queries.andLabelValueDate(); + } + + }, + + STATUS { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + params.add(sf.value.value); + return queries.findByStatus(); + } + }, + + LOCATION { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + params.add(sf.value.value); + return queries.findByLocation(); + } + }, + + UPDATED { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + addDateParams(sf, params); + return queries.findByUpdated(); + } + + }, + + UPDATED_BY { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + addUserToParam(context.currentUser, params, context.userNameToId, sf); + return queries.findByUpdatedBy(); + } + }, + + BOARD_STATUS { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + params.add(sf.value.value); + return queries.findByBoardStatus(); + } + }, + + FREETEXT { + @Override + public String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context) { + params.add(sf.value.value);// for card sequence number + params.add(sf.value.value);// for card name + params.add(sf.value.value);// for card data + return queries.findByFreeText(); + } + }; + + public abstract String toBaseQuery(SearchFilter sf, SearchQuery queries, List params, + SearchContext context); + + } + + private static void addDateParams(SearchFilter sf, List params) { + if (sf.value.type == ValueType.DATE_IDENTIFIER) { + fromDateIdentifier(sf.value.value.toString(), params); + } else { + Date from = null, to = null; + try { + String[] splitted = sf.value.value.toString().split(Pattern.quote("..")); + String[] format = { "dd.MM.yyyy", "dd-MM-yyyy", "dd/MM/yyyy", "yyyy-MM-dd", "yyyy.MM.dd", + "yyyy/MM/dd" }; + if (splitted.length == 2) { + from = truncate(parseDateStrictly(splitted[0], format), Calendar.DAY_OF_MONTH); + to = addDays(truncate(parseDateStrictly(splitted[1], format), Calendar.DAY_OF_MONTH), 1); + } else { + from = truncate(parseDateStrictly(sf.value.value.toString(), format), Calendar.DAY_OF_MONTH); + to = addDays(from, 1); + } + } catch (ParseException pe) { + // + } + params.add(from); + params.add(to); + } + } + + // TODO: refactor/cleanup + private static void fromDateIdentifier(String identifier, List params) { + Date todayTruncated = truncate(new Date(), Calendar.DAY_OF_MONTH); + Date beginMonth = truncate(new Date(), Calendar.MONTH); + + // TODO: internazionalization... + Calendar c = truncate(Calendar.getInstance(), Calendar.DAY_OF_MONTH); + c.setFirstDayOfWeek(Calendar.MONDAY); + c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + Date beginOfWeek = c.getTime(); + // + + switch (identifier) { + case "late": + params.add(new Date(0)); + params.add(todayTruncated); + break; + case "today": + params.add(todayTruncated); + params.add(addDays(todayTruncated, 1)); + break; + case "yesterday": + params.add(addDays(todayTruncated, -1)); + params.add(todayTruncated); + break; + case "tomorrow": + params.add(addDays(todayTruncated, 1)); + params.add(addDays(todayTruncated, 2)); + break; + case "this week": + params.add(beginOfWeek); + params.add(addWeeks(beginOfWeek, 1)); + break; + case "this month": + params.add(beginMonth); + params.add(addMonths(beginMonth, 1)); + break; + case "next week": + params.add(addWeeks(beginOfWeek, 1)); + params.add(addWeeks(beginOfWeek, 2)); + break; + case "next month": + params.add(addMonths(beginMonth, 1)); + params.add(addMonths(beginMonth, 2)); + break; + case "previous week": + params.add(addWeeks(beginOfWeek, -1)); + params.add(beginOfWeek); + break; + case "previous month": + params.add(addMonths(beginMonth, -1)); + params.add(beginMonth); + break; + case "last week": + params.add(addDays(todayTruncated, -6)); + params.add(addDays(todayTruncated, 1)); + break; + case "last month": + params.add(addDays(todayTruncated, -29)); + params.add(addDays(todayTruncated, 1)); + break; + default: + break; + } + } + + public enum ValueType { + BOOLEAN, STRING, CURRENT_USER, DATE_IDENTIFIER, UNASSIGNED + } + + @Getter + @AllArgsConstructor + public static class SearchFilterValue { + private final ValueType type; + private final Object value; + } + + private static void addUserToParam(UserWithPermission userWithPermission, List params, + Map userNameToId, SearchFilter searchFilter) { + if (searchFilter.value.type == ValueType.CURRENT_USER && "me".equals(searchFilter.value.value)) { + params.add(userWithPermission.getId()); + } else { + params.add(from(userNameToId, searchFilter.value.value)); + } + } + + private static Integer from(Map f, Object key) { + return key == null ? null : f.get(key); + } + + private static Integer tryParse(String value) { + try { + return Integer.valueOf(value, 10); + } catch (NullPointerException | NumberFormatException e) { + return null; + } + } + + public FilterType getType() { + return type; + } + + public String getName() { + return name; + } + + public SearchFilterValue getValue() { + return value; + } + +} diff --git a/src/main/java/io/lavagna/service/SearchService.java b/src/main/java/io/lavagna/service/SearchService.java new file mode 100644 index 000000000..e4d1aad30 --- /dev/null +++ b/src/main/java/io/lavagna/service/SearchService.java @@ -0,0 +1,293 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static io.lavagna.service.SearchFilter.filter; +import static io.lavagna.service.SearchFilter.filterByColumnDefinition; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.CardFull; +import io.lavagna.model.CardFullWithCounts; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.Permission; +import io.lavagna.model.SearchResults; +import io.lavagna.model.UserWithPermission; +import io.lavagna.query.SearchQuery; +import io.lavagna.service.SearchFilter.FilterType; +import io.lavagna.service.SearchFilter.SearchContext; +import io.lavagna.service.SearchFilter.SearchFilterValue; +import io.lavagna.service.SearchFilter.ValueType; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Clob; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StreamUtils; + +/** + * Service for searching Cards using criterias defined using {@link SearchFilter}. + */ +@Service +@Transactional(readOnly = true) +public class SearchService { + + private static final Logger LOG = LogManager.getLogger(); + + private static final int CARDS_PER_PAGE = 50; + + private final NamedParameterJdbcTemplate jdbc; + private final CardRepository cardRepository; + private final CardService cardService; + private final UserRepository userRepository; + private final ProjectService projectService; + private final BoardRepository boardRepository; + private final SearchQuery queries; + + @Autowired + public SearchService(CardRepository cardRepository, CardService cardService, UserRepository userRepository, + ProjectService projectService, BoardRepository boardRepository, NamedParameterJdbcTemplate jdbc, + SearchQuery queries) { + this.cardRepository = cardRepository; + this.cardService = cardService; + this.userRepository = userRepository; + this.projectService = projectService; + this.boardRepository = boardRepository; + this.jdbc = jdbc; + this.queries = queries; + } + + private List filtersAsList(SearchFilter locationFilter, + SearchFilter statusOpen, boolean excludeArchivedBoards) { + return excludeArchivedBoards ? + Arrays.asList(statusOpen, locationFilter, + filter(FilterType.BOARD_STATUS, SearchFilter.ValueType.BOOLEAN, Boolean.FALSE)) : + Arrays.asList(statusOpen, locationFilter); + } + + public Map findTaksByColumnDefinition(Integer projectId, Integer boardId, + boolean excludeArchivedBoards, UserWithPermission user) { + SearchFilter locationFilter = filter(SearchFilter.FilterType.LOCATION, SearchFilter.ValueType.STRING, + BoardColumn.BoardColumnLocation.BOARD.toString()); + + Map results = new EnumMap<>(ColumnDefinition.class); + + SearchFilter statusOpen = filterByColumnDefinition(ColumnDefinition.OPEN); + SearchResults openTasks = find(filtersAsList(locationFilter, statusOpen, excludeArchivedBoards), projectId, + boardId, user, 0); + results.put(ColumnDefinition.OPEN, openTasks.getCount()); + + SearchFilter statusClosed = filterByColumnDefinition(ColumnDefinition.CLOSED); + SearchResults closedTasks = find(filtersAsList(locationFilter, statusClosed, excludeArchivedBoards), projectId, + boardId, user, 0); + results.put(ColumnDefinition.CLOSED, closedTasks.getCount()); + + SearchFilter statusBacklog = filterByColumnDefinition(ColumnDefinition.BACKLOG); + SearchResults backlogTasks = find(filtersAsList(locationFilter, statusBacklog, excludeArchivedBoards), + projectId, boardId, user, 0); + results.put(ColumnDefinition.BACKLOG, backlogTasks.getCount()); + + SearchFilter statusDeferred = filterByColumnDefinition(ColumnDefinition.DEFERRED); + SearchResults deferredTasks = find(filtersAsList(locationFilter, statusDeferred, excludeArchivedBoards), + projectId, boardId, user, 0); + results.put(ColumnDefinition.DEFERRED, deferredTasks.getCount()); + + return results; + } + + public SearchResults find(List unmergedSearchFilter, Integer projectId, Integer boardId, + UserWithPermission currentUser, int page) { + + // if a user don't have access to the specified project id we skip the + // whole search + final boolean userHasNotProjectAccess = projectId != null + && !currentUser.getBasePermissions().containsKey(Permission.READ) + && !currentUser.projectsWithPermission(Permission.READ).contains( + projectService.findById(projectId).getShortName()); + final boolean userHasNoReadAccess = projectId == null + && !currentUser.getBasePermissions().containsKey(Permission.READ) + && currentUser.projectsWithPermission(Permission.READ).isEmpty(); + + final boolean noProjectIdForBoardId = projectId == null && boardId != null; + + final boolean boardIsntInProject = projectId != null && boardId != null + && boardRepository.findBoardById(boardId).getProjectId() != projectId; + + if (userHasNotProjectAccess || userHasNoReadAccess || noProjectIdForBoardId || boardIsntInProject) { + return new SearchResults(Collections.emptyList(), 0, page, CARDS_PER_PAGE); + } + + List searchFilters = mergeFreeTextFilters(unmergedSearchFilter); + // + + List params = new ArrayList<>(); + int filteringConditionsCount = 0; + // + + List usersOrCardToSearch = new ArrayList<>(); + // fetch all possible user->id, card->id in the value types with string + // (thus unknown use) + for (SearchFilter searchFilter : searchFilters) { + if (searchFilter.getValue() != null && searchFilter.getValue().getType() == ValueType.STRING) { + usersOrCardToSearch.add(searchFilter.getValue().getValue().toString()); + } + } + // + + Map cardNameToId = cardRepository.findCardsIds(usersOrCardToSearch); + Map userNameToId = userRepository.findUsersId(usersOrCardToSearch); + + SearchContext searchContext = new SearchContext(currentUser, userNameToId, cardNameToId); + + // + + StringBuilder baseQuery = new StringBuilder(queries.findFirstFrom()).append("SELECT CARD_ID FROM ( "); + + // add filter conditions + for (int i = 0; i < searchFilters.size(); i++) { + SearchFilter searchFilter = searchFilters.get(i); + + String filterConditionQuery = searchFilter.getType().toBaseQuery(searchFilter, queries, params, + searchContext); + + baseQuery.append("( ").append(filterConditionQuery).append(" ) "); + if (i < searchFilters.size() - 1) { + baseQuery.append(" UNION ALL "); + } + filteringConditionsCount++; + } + // + + /* AS CARD_IDS -> table alias for mysql */ + baseQuery.append(" ) AS CARD_IDS GROUP BY CARD_ID HAVING COUNT(CARD_ID) = ?").append(queries.findSecond()); + params.add(filteringConditionsCount); + + if (boardId != null) { + baseQuery.append(queries.findThirdWhere()).append(queries.findFourthInBoardId()); + params.add(boardId); + } else if (projectId != null) { + baseQuery.append(queries.findThirdWhere()).append(queries.findInFifthProjectId()); + params.add(projectId); + } else if (!currentUser.getBasePermissions().containsKey(Permission.READ)) { + + Set projectsWithPermission = currentUser.projectsIdWithPermission(Permission.READ); + baseQuery.append(queries.findThirdWhere()).append(queries.findSixthRestrictedReadAccess()).append(" (") + .append(StringUtils.repeat("?", " , ", projectsWithPermission.size())).append(" ) "); + + params.addAll(projectsWithPermission); + } + + params.add(CARDS_PER_PAGE + 1);// limit + params.add(page * CARDS_PER_PAGE);// offset + + String findCardsQuery = queries.findFirstSelect() + baseQuery.toString() + queries.findSeventhOrderByAndLimit(); + + List sr = jdbc.getJdbcOperations().queryForList(findCardsQuery, params.toArray(), Integer.class); + + // + + int count = sr.size(); + if (page == 0 && sr.size() == (CARDS_PER_PAGE + 1) || page > 0) { + String countCardsQuery = queries.findFirstSelectCount() + baseQuery.toString(); + count = jdbc.getJdbcOperations().queryForObject(countCardsQuery, + params.subList(0, params.size() - 2).toArray(), Integer.class); + } + + // + return new SearchResults(cardFullWithCounts(sr), count, page, CARDS_PER_PAGE); + } + + private List cardFullWithCounts(List sr) { + + if (sr.isEmpty()) { + return Collections.emptyList(); + } + + // super ugly :( + Map idToPosition = new HashMap<>(); + for (int i = 0; i < sr.size(); i++) { + idToPosition.put(sr.get(i), i); + } + + CardFull[] orderedCf = new CardFull[sr.size()]; + // reorder: + for (CardFull cf : cardRepository.findAllByIds(sr)) { + orderedCf[idToPosition.get(cf.getId())] = cf; + } + // + + return cardService.fetchCardFull(Arrays.asList(orderedCf)); + } + + private static List mergeFreeTextFilters(List unmergedSearchFilter) { + List merged = new ArrayList<>(unmergedSearchFilter.size()); + StringBuilder sb = new StringBuilder(); + for (SearchFilter sf : unmergedSearchFilter) { + if (sf.getType() != FilterType.FREETEXT) { + merged.add(sf); + } else { + sb.append(" ").append(sf.getValue().getValue()); + } + } + + String freeText = sb.toString().trim(); + if (freeText.length() > 0) { + merged.add(new SearchFilter(FilterType.FREETEXT, null, new SearchFilterValue(ValueType.STRING, freeText))); + } + + return merged; + } + + // used by HSQLDB, obviously not optimized at all (as it's only for dev + // purpose) + public static boolean searchText(String data, String toSearch) { + String[] wordsToSearch = toSearch.split("\\s+"); + String lowerCasedData = data.toLowerCase(Locale.ENGLISH); + for (String word : wordsToSearch) { + if (!lowerCasedData.contains(word.toLowerCase(Locale.ENGLISH))) { + return false; + } + } + return true; + } + + public static boolean searchTextClob(Clob data, String toSearch) { + try (InputStream is = data.getAsciiStream()) { + String res = StreamUtils.copyToString(is, StandardCharsets.UTF_8); + return searchText(res, toSearch); + } catch (IOException | SQLException e) { + LOG.warn("error while reading clob", e); + return false; + } + } +} diff --git a/src/main/java/io/lavagna/service/SetupService.java b/src/main/java/io/lavagna/service/SetupService.java new file mode 100644 index 000000000..1bc6a0c26 --- /dev/null +++ b/src/main/java/io/lavagna/service/SetupService.java @@ -0,0 +1,45 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.ConfigurationKeyValue; +import io.lavagna.model.UserToCreate; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class SetupService { + + private final ConfigurationRepository configurationRepository; + private final UserService userService; + + @Autowired + public SetupService(ConfigurationRepository configurationRepository, UserService userService) { + this.configurationRepository = configurationRepository; + this.userService = userService; + } + + public void updateConfiguration(List toUpdateOrCreate, UserToCreate user) { + configurationRepository.updateOrCreate(toUpdateOrCreate); + userService.createUser(user); + } +} diff --git a/src/main/java/io/lavagna/service/StatisticsService.java b/src/main/java/io/lavagna/service/StatisticsService.java new file mode 100644 index 000000000..1fc3a464f --- /dev/null +++ b/src/main/java/io/lavagna/service/StatisticsService.java @@ -0,0 +1,167 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import io.lavagna.model.CardFull; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.EventsCount; +import io.lavagna.model.LabelAndValueWithCount; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.MilestoneCount; +import io.lavagna.model.Pair; +import io.lavagna.model.StatisticsResult; +import io.lavagna.query.StatisticsQuery; + +import java.util.Date; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class StatisticsService { + + private final StatisticsQuery queries; + + @Autowired + public StatisticsService(StatisticsQuery queries) { + this.queries = queries; + } + + @Scheduled(cron = "30 59 23,5,11,17 * * *") + @Transactional(readOnly = false) + public void snapshotCardsStatus() { + + queries.snapshotCardsStatus(new Date()); + + queries.cleanOldCardsStatusSnapshots(); + } + + private Map> toStatusByDay(List results) { + Map> statusByDay = new HashMap<>(); + for (StatisticsResult result : results) { + if (!statusByDay.containsKey(result.getDay())) { + statusByDay.put(result.getDay(), new EnumMap(ColumnDefinition.class)); + } + Map day = statusByDay.get(result.getDay()); + day.put(result.getColumnDefinition(), result.getCount()); + } + return statusByDay; + } + + public Map> getCardsStatusByBoard(int boardId, Date fromDate) { + return toStatusByDay(queries.getCardsStatusByBoard(boardId, fromDate)); + } + + public Map> getCardsStatusByProject(int projectId, Date fromDate) { + return toStatusByDay(queries.getCardsStatusByProject(projectId, fromDate)); + } + + public Integer getActiveUsersOnBoard(int boardId, Date fromDate) { + return queries.getActiveUsersOnBoard(boardId, fromDate); + } + + public Integer getActiveUsersOnProject(int projectId, Date fromDate) { + return queries.getActiveUsersOnProject(projectId, fromDate); + } + + // Average users per card + + public double getAverageUsersPerCardOnBoard(int boardId) { + return ObjectUtils.firstNonNull(queries.getAverageUsersPerCardOnBoard(boardId), 0d); + } + + public double getAverageUsersPerCardOnProject(int projectId) { + return ObjectUtils.firstNonNull(queries.getAverageUsersPerCardOnProject(projectId), 0d); + } + + // Average cards per user + + public double getAverageCardsPerUserOnBoard(int boardId) { + return ObjectUtils.firstNonNull(queries.getAverageCardsPerUserOnBoard(boardId), 0d); + } + + public double getAverageCardsPerUserOnProject(int projectId) { + return ObjectUtils.firstNonNull(queries.getAverageCardsPerUserOnProject(projectId), 0d); + } + + // Cards by label + + public List getCardsByLabelOnBoard(int boardId) { + return queries.getCardsByLabelOnBoard(boardId); + } + + public List getCardsByLabelOnProject(int projectId) { + return queries.getCardsByLabelOnProject(projectId); + } + + // Created / closed cards + + private Map> mergeCounts(List createdCards, List closedCards) { + Map> counts = new HashMap<>(); + for (EventsCount count : createdCards) { + counts.put(count.getDate(), new Pair<>(count.getCount(), 0l)); + } + for (EventsCount count : closedCards) { + long created = 0; + if (counts.containsKey(count.getDate())) { + created = counts.get(count.getDate()).getFirst(); + counts.remove(count.getDate()); + } + counts.put(count.getDate(), new Pair<>(created, count.getCount())); + } + return counts; + } + + public Map> getCreatedAndClosedCardsByBoard(int boardId, Date fromDate) { + return mergeCounts(queries.getCreatedCardsByBoard(boardId, fromDate), + queries.getClosedCardsByBoard(boardId, fromDate)); + } + + public Map> getCreatedAndClosedCardsByProject(int projectId, Date fromDate) { + return mergeCounts(queries.getCreatedCardsByProject(projectId, fromDate), + queries.getClosedCardsByProject(projectId, fromDate)); + } + + // Most active card + + public CardFull getMostActiveCardByBoard(int boardId, Date fromDate) { + return queries.getMostActiveCardByBoard(boardId, fromDate); + } + + public CardFull getMostActiveCardByProject(int projectId, Date fromDate) { + return queries.getMostActiveCardByProject(projectId, fromDate); + } + + // Milestones + + public Map> getAssignedAndClosedCardsByMilestone(LabelListValue milestone, Date fromDate) { + return mergeCounts(queries.getAssignedCardsByMilestone(milestone.getValue(), fromDate), + queries.getClosedCardsByMilestone(milestone.getId(), fromDate)); + } + + public List findCardsCountByMilestone(int projectId) { + return queries.findCardsCountByMilestone(projectId); + } +} diff --git a/src/main/java/io/lavagna/service/SupportedEventType.java b/src/main/java/io/lavagna/service/SupportedEventType.java new file mode 100644 index 000000000..56cfe52a9 --- /dev/null +++ b/src/main/java/io/lavagna/service/SupportedEventType.java @@ -0,0 +1,263 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.ArrayUtils.toArray; +import static org.apache.commons.lang3.ObjectUtils.firstNonNull; +import static org.apache.commons.lang3.tuple.ImmutablePair.of; +import io.lavagna.model.CardDataMetadata; +import io.lavagna.model.CardType; +import io.lavagna.model.Event; + +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +/** + * A subset of the Event enum where the event is mapped to a text to be sent to the user. + */ +enum SupportedEventType { + CARD_UPDATE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), e.getValueString()); + } + + }, + COMMENT_CREATE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray( + context.formatUser(e.getUserId()), + firstNonNull( + findFirstContentInHistory(e.getDataId(), CardType.COMMENT_HISTORY, cardDataRepository), + context.cardData.get(e.getDataId()))); + } + + }, + DESCRIPTION_CREATE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray( + context.formatUser(e.getUserId()), + firstNonNull( + findFirstContentInHistory(e.getDataId(), CardType.DESCRIPTION_HISTORY, cardDataRepository), + context.cardData.get(e.getDataId()))); + } + + }, + COMMENT_UPDATE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray( + context.formatUser(e.getUserId()), + firstNonNull(findNextContentInHistory(e.getPreviousDataId(), cardDataRepository), + context.cardData.get(e.getDataId())), context.cardData.get(e.getPreviousDataId())); + } + + }, + COMMENT_DELETE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getPreviousDataId()), + context.cardData.get(e.getDataId())); + } + }, + DESCRIPTION_UPDATE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray( + context.formatUser(e.getUserId()), + firstNonNull(findNextContentInHistory(e.getPreviousDataId(), cardDataRepository), + context.cardData.get(e.getDataId())), context.cardData.get(e.getPreviousDataId())); + } + }, + CARD_ARCHIVE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.formatColumn(e.getPreviousColumnId()), + context.formatColumn(e.getColumnId())); + } + }, + CARD_BACKLOG { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.formatColumn(e.getPreviousColumnId()), + context.formatColumn(e.getColumnId())); + } + }, + CARD_TRASH { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.formatColumn(e.getPreviousColumnId()), + context.formatColumn(e.getColumnId())); + } + + }, + CARD_MOVE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.formatColumn(e.getPreviousColumnId()), + context.formatColumn(e.getColumnId())); + } + + }, + FILE_UPLOAD { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), e.getValueString()); + } + + }, + FILE_DELETE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), e.getValueString()); + } + }, + ACTION_LIST_CREATE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId())); + } + }, + ACTION_LIST_DELETE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId())); + } + }, + ACTION_ITEM_CREATE { + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId()), + context.cardData.get(e.getPreviousDataId())); + } + }, + ACTION_ITEM_DELETE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId()), + context.cardData.get(e.getPreviousDataId())); + } + + }, + ACTION_ITEM_CHECK { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId()), + context.cardData.get(e.getPreviousDataId())); + } + }, + ACTION_ITEM_UNCHECK { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId()), + context.cardData.get(e.getPreviousDataId())); + } + }, + ACTION_ITEM_MOVE { + + @Override + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + String newActionListName = cardDataRepository.getUndeletedDataLightById( + cardDataRepository.getUndeletedDataLightById(e.getDataId()).getReferenceId()).getContent(); + return toArray(context.formatUser(e.getUserId()), context.cardData.get(e.getDataId()), newActionListName); + } + }, + LABEL_CREATE { + @Override + protected ImmutablePair toKeyAndParam(Event e, EventsContext context, + CardDataRepository cardDataRepository) { + Map messages = new HashMap(); + messages.put("MILESTONE", "User %s has added a Milestone: %s"); + messages.put("DUE_DATE", "User %s added a due date for: %s"); + messages.put("ASSIGNED", "User %s assigned the card to: %s"); + messages.put("WATCHED_BY", "User %s is now watching this card"); + return handleLabelCreationAndDeletion(e, context, messages, this.name()); + } + }, + LABEL_DELETE { + @Override + protected ImmutablePair toKeyAndParam(Event e, EventsContext context, + CardDataRepository cardDataRepository) { + Map messages = new HashMap(); + messages.put("MILESTONE", "User %s removed a Milestone: %s"); + messages.put("DUE_DATE", "User %s removed a due date for %s"); + messages.put("ASSIGNED", "User %s removed %s from the assigned users"); + messages.put("WATCHED_BY", "User %s is not watching this card anymore"); + return handleLabelCreationAndDeletion(e, context, messages, this.name()); + } + }; + + private static ImmutablePair handleLabelCreationAndDeletion(Event e, EventsContext context, + Map msg, String defaultMessage) { + if ("MILESTONE".equals(e.getLabelName())) { + return of("event." + defaultMessage + ".MILESTONE", + toArray(context.formatUser(e.getUserId()), e.getValueString())); + } else if ("DUE_DATE".equals(e.getLabelName())) { + return of( + "event." + defaultMessage + ".DUE_DATE", + toArray(context.formatUser(e.getUserId()), + new SimpleDateFormat("dd.MM.yyyy").format(e.getValueTimestamp()))); + } else if ("ASSIGNED".equals(e.getLabelName())) { + return of("event." + defaultMessage + ".ASSIGNED", + toArray(context.formatUser(e.getUserId()), context.formatUser(e.getValueUser()))); + } else if ("WATCHED_BY".equals(e.getLabelName())) { + return of("event." + defaultMessage + ".WATCHED_BY", toArray(context.formatUser(e.getUserId()))); + } else { + return of("event." + defaultMessage, toArray(context.formatUser(e.getUserId()), context.formatLabel(e))); + } + } + + protected String[] params(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return null; + } + + ImmutablePair toKeyAndParam(Event e, EventsContext context, CardDataRepository cardDataRepository) { + return of("event." + this.name(), params(e, context, cardDataRepository)); + } + + private static String findFirstContentInHistory(int id, CardType type, CardDataRepository cardDataRepository) { + CardDataMetadata m = cardDataRepository.findMetadataById(id); + return cardDataRepository.findContentWith(m.getCardId(), m.getId(), type, 1); + } + + private static String findNextContentInHistory(int id, CardDataRepository cardDataRepository) { + CardDataMetadata m = cardDataRepository.findMetadataById(id); + return cardDataRepository.findContentWith(m.getCardId(), m.getReferenceId(), m.getType(), m.getOrder() + 1); + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/service/UserRepository.java b/src/main/java/io/lavagna/service/UserRepository.java new file mode 100644 index 000000000..3d7880f36 --- /dev/null +++ b/src/main/java/io/lavagna/service/UserRepository.java @@ -0,0 +1,180 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static org.apache.commons.lang3.StringUtils.trimToNull; +import io.lavagna.model.Permission; +import io.lavagna.model.User; +import io.lavagna.query.UserQuery; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * CRUD operation over {@link User} + */ +@Repository +@Transactional(readOnly = true) +public class UserRepository { + + private final NamedParameterJdbcTemplate jdbc; + private final UserQuery queries; + + @Autowired + public UserRepository(NamedParameterJdbcTemplate jdbc, UserQuery queries) { + this.jdbc = jdbc; + this.queries = queries; + } + + public User findUserByName(String provider, String name) { + return queries.findUserByName(provider, name); + } + + public User findById(int id) { + return queries.findUserById(id); + } + + public List findByIds(Collection ids) { + return ids.isEmpty() ? Collections. emptyList() : queries.findByIds(ids); + } + + public boolean userExistsAndEnabled(String provider, String name) { + return !Integer.valueOf(0).equals(queries.userExistsAndEnabled(provider, name, true)); + } + + public boolean userExists(String provider, String name) { + return !Integer.valueOf(0).equals(queries.userExistsAndEnabled(provider, name)); + } + + public List findUsers(String criteria) { + return queries.findUsers(criteria); + } + + public List findUsers(String criteria, int projectId, Permission permission) { + return queries.findUsers(criteria, projectId, permission.toString());// + } + + @Transactional(readOnly = false) + public void createUsers(Collection users) { + List params = new ArrayList<>(users.size()); + for (User user : users) { + params.add(prepareUserParameterSource(user)); + } + jdbc.batchUpdate(queries.createUserFull(), params.toArray(new SqlParameterSource[params.size()])); + } + + private static SqlParameterSource prepareUserParameterSource(User user) { + return new MapSqlParameterSource("provider", trimToNull(user.getProvider())) + .addValue("userName", trimToNull(user.getUsername())).addValue("email", trimToNull(user.getEmail())) + .addValue("displayName", trimToNull(user.getDisplayName())).addValue("enabled", user.isEnabled()) + .addValue("emailNotification", user.isEmailNotification()) + .addValue("memberSince", ObjectUtils.firstNonNull(user.getMemberSince(), new Date())); + } + + @Transactional(readOnly = false) + public int createUser(String provider, String userName, String email, String displayName, boolean enabled) { + return queries.createUser(provider, userName, email, displayName, enabled); + } + + @Transactional(readOnly = false) + public int updateProfile(User user, String email, String displayName, boolean emailNotification) { + return queries.updateProfile(trimToNull(email), trimToNull(displayName), emailNotification, user.getId()); + } + + @Transactional(readOnly = false) + public int toggle(int userId, boolean enabled) { + return queries.toggle(enabled, userId); + } + + public List findAll() { + return queries.findAll(); + } + + public Map findUsersId(List users) { + + List usersToFind = new ArrayList<>(users.size()); + for (String user : users) { + String[] splittedString = StringUtils.split(user, ':'); + if (splittedString.length > 1) { + String provider = splittedString[0]; + String username = StringUtils.join(ArrayUtils.subarray(splittedString, 1, splittedString.length), ':'); + usersToFind.add(new String[] { provider, username }); + } + } + + if (usersToFind.isEmpty()) { + return Collections.emptyMap(); + } + + final Map res = new HashMap<>(); + MapSqlParameterSource param = new MapSqlParameterSource("users", usersToFind); + + jdbc.query(queries.findUsersId(), param, new RowCallbackHandler() { + @Override + public void processRow(ResultSet rs) throws SQLException { + res.put(rs.getString("PROVIDER_USER"), rs.getInt("USER_ID")); + } + }); + + return res; + } + + @Transactional(readOnly = false) + public String createRememberMeToken(int userId) { + String token = UUID.randomUUID().toString();// <- this use secure random + String hashedToken = DigestUtils.sha256Hex(token); + + queries.registerRememberMeToken(hashedToken, userId, new Date()); + + return token; + } + + @Transactional(readOnly = false) + public void deleteRememberMeToken(int userId, String token) { + queries.deleteToken(DigestUtils.sha256Hex(token), userId); + } + + public boolean rememberMeTokenExists(int userId, String token) { + String hashedToken = DigestUtils.sha256Hex(token); + return queries.tokenExists(hashedToken, userId).equals(1); + } + + @Transactional(readOnly = false) + public void clearAllTokens(User currentUser) { + queries.deleteAllTokensForUserId(currentUser.getId()); + } +} diff --git a/src/main/java/io/lavagna/service/UserService.java b/src/main/java/io/lavagna/service/UserService.java new file mode 100644 index 000000000..e6962ef28 --- /dev/null +++ b/src/main/java/io/lavagna/service/UserService.java @@ -0,0 +1,88 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import static java.util.Objects.requireNonNull; +import io.lavagna.model.Permission; +import io.lavagna.model.Role; +import io.lavagna.model.User; +import io.lavagna.model.UserToCreate; +import io.lavagna.model.UserWithPermission; +import io.lavagna.service.PermissionService.ProjectRoleAndPermissionFullHolder; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link User} related operations. + */ +@Service +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + private final PermissionService permissionService; + + @Autowired + public UserService(UserRepository userRepository, PermissionService permissionService) { + this.userRepository = userRepository; + this.permissionService = permissionService; + } + + @Transactional(readOnly = false) + public void createUser(UserToCreate userToCreate) { + requireNonNull(userToCreate); + requireNonNull(userToCreate.getProvider()); + requireNonNull(userToCreate.getUsername()); + + userRepository.createUser(userToCreate.getProvider(), userToCreate.getUsername(), userToCreate.getEmail(), + userToCreate.getDisplayName(), userToCreate.isEnabled()); + + if (userToCreate.getRoles() == null) { + return; + } + + User u = userRepository.findUserByName(userToCreate.getProvider(), userToCreate.getUsername()); + Set userId = Collections.singleton(u.getId()); + for (String r : userToCreate.getRoles()) { + permissionService.assignRoleToUsers(new Role(r), userId); + } + } + + public UserWithPermission findUserWithPermission(int userId) { + User user = userRepository.findById(userId); + + Set permissions = permissionService.findBasePermissionByUserId(user.getId()); + ProjectRoleAndPermissionFullHolder permissionsHolder = permissionService + .findPermissionsGroupedByProjectForUserId(user.getId()); + return new UserWithPermission(user, permissions, permissionsHolder.getPermissionsByProject(), + permissionsHolder.getPermissionsByProjectId()); + } + + @Transactional(readOnly = false) + public void createUsers(List usersToCreate) { + for (UserToCreate utc : requireNonNull(usersToCreate)) { + createUser(utc); + } + } + +} diff --git a/src/main/java/io/lavagna/service/Utils.java b/src/main/java/io/lavagna/service/Utils.java new file mode 100644 index 000000000..7b3149876 --- /dev/null +++ b/src/main/java/io/lavagna/service/Utils.java @@ -0,0 +1,33 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service; + +import java.util.ArrayList; +import java.util.List; + +abstract class Utils { + + static List filter(List ids, List toKeep) { + List r = new ArrayList<>(); + for (Integer id : ids) { + if (toKeep.contains(id)) { + r.add(id); + } + } + return r; + } +} diff --git a/src/main/java/io/lavagna/service/importexport/AbstractProcessEvent.java b/src/main/java/io/lavagna/service/importexport/AbstractProcessEvent.java new file mode 100644 index 000000000..557a52d12 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/AbstractProcessEvent.java @@ -0,0 +1,51 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +abstract class AbstractProcessEvent { + + protected final CardRepository cardRepository; + protected final UserRepository userRepository; + protected final CardDataService cardDataService; + + AbstractProcessEvent(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + this.cardRepository = cardRepository; + this.userRepository = userRepository; + this.cardDataService = cardDataService; + } + + abstract void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile); + + protected int cardId(EventFull e) { + return cardRepository.findCardIdByBoardNameAndSeq(e.getBoardShortName(), e.getCardSequenceNumber()); + } + + protected User toUser(EventFull e) { + return userRepository.findUserByName(e.getUserProvider(), e.getUsername()); + } +} diff --git a/src/main/java/io/lavagna/service/importexport/AbstractProcessLabelEvent.java b/src/main/java/io/lavagna/service/importexport/AbstractProcessLabelEvent.java new file mode 100644 index 000000000..244475528 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/AbstractProcessLabelEvent.java @@ -0,0 +1,130 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import java.util.Date; +import java.util.List; + +import io.lavagna.model.Board; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.CardLabel.LabelType; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Event.EventType; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.EventRepository; +import io.lavagna.service.LabelService; +import io.lavagna.service.UserRepository; + +abstract class AbstractProcessLabelEvent extends AbstractProcessEvent { + + protected final LabelService labelService; + protected final CardLabelRepository cardLabelRepository; + protected final BoardRepository boardRepository; + private final EventRepository eventRepository; + + AbstractProcessLabelEvent(CardRepository cardRepository, UserRepository userRepository, + CardDataService cardDataService, LabelService labelService, CardLabelRepository cardLabelRepository, + BoardRepository boardRepository, EventRepository eventRepository) { + super(cardRepository, userRepository, cardDataService); + this.labelService = labelService; + this.cardLabelRepository = cardLabelRepository; + this.boardRepository = boardRepository; + this.eventRepository = eventRepository; + } + + protected CardLabelValue findCardLabelValueBy(EventFull e) { + CardLabel cl = findLabelByEvent(e); + if (cl != null) { + LabelValue lv = labelValue(cl, e); + if(lv == null) { + return null; + } + List r = cardLabelRepository.findLabelValueByLabelAndValue(cardId(e), cl, lv); + return r.size() == 1 ? r.get(0) : null; + } + return null; + } + + protected LabelValue labelValue(CardLabel cl, EventFull e) { + Event event = e.getEvent(); + if (cl.getType() == LabelType.LIST) { + List res = cardLabelRepository.findListValuesByLabelIdAndValue(cl.getId(), e.getEvent() + .getValueString()); + return (res.size() == 1) ? new LabelValue(null, null, null, null, null, res.get(0).getId()) : null; + } else if (cl.getType() == LabelType.USER) { + return new LabelValue(null, null, null, null, userRepository.findUserByName(e.getLabelUserProvider(), + e.getLabelUsername()).getId(), null); + } else if (cl.getType() == LabelType.CARD) { + return new LabelValue(null, null, null, cardRepository.findCardIdByBoardNameAndSeq( + e.getLabelBoardShortName(), e.getLabelCardSequenceNumber()), null, null); + } else { + return new LabelValue(event.getValueString(), event.getValueTimestamp(), event.getValueInt(), null, null, + null); + } + + } + + protected CardLabel findLabelByEvent(EventFull e) { + Board b = boardRepository.findBoardByShortName(e.getBoardShortName()); + LabelDomain domain = CardLabel.RESERVED_SYSTEM_LABELS_NAME.contains(e.getEvent().getLabelName()) ? LabelDomain.SYSTEM + : LabelDomain.USER; + List r = cardLabelRepository.findLabelsByName(b.getProjectId(), e.getEvent().getLabelName(), domain); + return r.isEmpty() ? null : r.get(0); + } + + protected Integer fromLabelUsernameToUserId(EventFull e) { + if (e.getLabelUsername() != null && e.getLabelCardSequenceNumber() != null + && userRepository.userExists(e.getLabelUserProvider(), e.getLabelUsername())) { + return userRepository.findUserByName(e.getLabelUserProvider(), e.getLabelUsername()).getId(); + } + return null; + } + + protected Integer fromLabelCardToCardId(EventFull e) { + if (e.getLabelBoardShortName() != null && e.getLabelCardSequenceNumber() != null + && cardRepository.existCardWith(e.getLabelBoardShortName(), e.getLabelCardSequenceNumber())) { + return cardRepository.findCardIdByBoardNameAndSeq(e.getLabelBoardShortName(), + e.getLabelCardSequenceNumber()); + } + return null; + } + + protected void insertLabelEvent(EventFull e, Event event, Date time, EventType eventType) { + Integer labelCardId = fromLabelCardToCardId(e); + Integer labeUserId = fromLabelUsernameToUserId(e); + + LabelValue labelValue = new LabelValue(event.getValueString(), event.getValueTimestamp(), event.getValueInt(), + labelCardId, labeUserId, null); + + if ((event.getLabelType() == LabelType.CARD && labelCardId == null) + || (event.getLabelType() == LabelType.USER && labeUserId == null)) { + return; + } + + eventRepository.insertLabelEvent(event.getLabelName(), cardId(e), toUser(e).getId(), eventType, labelValue, + event.getLabelType(), time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ActionItemCheckUncheck.java b/src/main/java/io/lavagna/service/importexport/ActionItemCheckUncheck.java new file mode 100644 index 000000000..74c2241aa --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ActionItemCheckUncheck.java @@ -0,0 +1,43 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.model.Event.EventType; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class ActionItemCheckUncheck extends AbstractProcessEvent { + + ActionItemCheckUncheck(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + boolean toggled = event.getEvent() == EventType.ACTION_ITEM_CHECK; + cardDataService.toggleActionItem(context.getActionItemId().get(event.getDataId()), toggled, user, time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ActionItemCreate.java b/src/main/java/io/lavagna/service/importexport/ActionItemCreate.java new file mode 100644 index 000000000..68861f870 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ActionItemCreate.java @@ -0,0 +1,45 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.CardData; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class ActionItemCreate extends AbstractProcessEvent { + + public ActionItemCreate(CardRepository cardRepository, UserRepository userRepository, + CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + CardData cd = cardDataService.createActionItem(cardId(e), + context.getActionListId().get(event.getPreviousDataId()), e.getContent(), user, time); + context.getActionItemId().put(event.getDataId(), cd.getId()); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ActionItemDelete.java b/src/main/java/io/lavagna/service/importexport/ActionItemDelete.java new file mode 100644 index 000000000..ad8b82676 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ActionItemDelete.java @@ -0,0 +1,42 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class ActionItemDelete extends AbstractProcessEvent { + + public ActionItemDelete(CardRepository cardRepository, UserRepository userRepository, + CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + cardDataService.deleteActionItem(context.getActionItemId().get(event.getDataId()), user, time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ActionItemMove.java b/src/main/java/io/lavagna/service/importexport/ActionItemMove.java new file mode 100644 index 000000000..3a74a575d --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ActionItemMove.java @@ -0,0 +1,44 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Date; + +class ActionItemMove extends AbstractProcessEvent { + + ActionItemMove(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + int actionItemId = context.getActionItemId().get(event.getDataId()); + cardDataService.moveActionItem(cardId(e), actionItemId, context.getActionListId().get(event.getNewDataId()), + Collections.singletonList(actionItemId), user, time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ActionListCreate.java b/src/main/java/io/lavagna/service/importexport/ActionListCreate.java new file mode 100644 index 000000000..e86b20560 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ActionListCreate.java @@ -0,0 +1,43 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.CardData; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class ActionListCreate extends AbstractProcessEvent { + + ActionListCreate(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + CardData cd = cardDataService.createActionList(cardId(e), e.getContent(), user, time); + context.getActionListId().put(event.getDataId(), cd.getId()); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ActionListDelete.java b/src/main/java/io/lavagna/service/importexport/ActionListDelete.java new file mode 100644 index 000000000..11da8f866 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ActionListDelete.java @@ -0,0 +1,41 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class ActionListDelete extends AbstractProcessEvent { + + ActionListDelete(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + cardDataService.deleteActionList(context.getActionListId().get(event.getDataId()), user, time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/CardArchiveBacklogTrash.java b/src/main/java/io/lavagna/service/importexport/CardArchiveBacklogTrash.java new file mode 100644 index 000000000..a87887487 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CardArchiveBacklogTrash.java @@ -0,0 +1,58 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import static java.util.Collections.singletonList; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.CardService; +import io.lavagna.service.EventRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class CardArchiveBacklogTrash extends AbstractProcessEvent { + + private final CardService cardService; + private final EventRepository eventRepository; + + CardArchiveBacklogTrash(CardRepository cardRepository, UserRepository userRepository, + CardDataService cardDataService, CardService cardService, EventRepository eventRepository) { + super(cardRepository, userRepository, cardDataService); + this.cardService = cardService; + this.eventRepository = eventRepository; + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + int columnId = context.getColumns().get(e.getEvent().getColumnId()); + + if (event.getPreviousColumnId() == null) { + eventRepository.insertCardEvent(singletonList(cardId(e)), columnId, user.getId(), event.getEvent(), time); + } else { + int previousColumnId = context.getColumns().get(event.getPreviousColumnId()); + cardService.moveCardsToColumn(singletonList(cardId(e)), previousColumnId, columnId, user.getId(), + event.getEvent(), time); + } + + } +} diff --git a/src/main/java/io/lavagna/service/importexport/CardCreate.java b/src/main/java/io/lavagna/service/importexport/CardCreate.java new file mode 100644 index 000000000..e797a7edc --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CardCreate.java @@ -0,0 +1,49 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import java.nio.file.Path; +import java.util.Date; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.CardService; +import io.lavagna.service.UserRepository; + +class CardCreate extends AbstractProcessEvent { + + private final CardService cardService; + + CardCreate(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService, + CardService cardService) { + super(cardRepository, userRepository, cardDataService); + this.cardService = cardService; + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + Integer columnId = context.getColumns().get(e.getEvent().getColumnId()); + if (columnId != null) { + cardService.createCard(event.getValueString(), columnId, time, user); + } + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/CardMove.java b/src/main/java/io/lavagna/service/importexport/CardMove.java new file mode 100644 index 000000000..4d1400f2a --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CardMove.java @@ -0,0 +1,51 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.CardService; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class CardMove extends AbstractProcessEvent { + + private final CardService cardService; + + CardMove(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService, + CardService cardService) { + super(cardRepository, userRepository, cardDataService); + this.cardService = cardService; + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + Integer columnId = null; + if (event.getPreviousColumnId() == null || (columnId = context.getColumns().get(event.getColumnId())) == null) { + return; + } + int previousColumnId = context.getColumns().get(event.getPreviousColumnId()); + cardService.moveCardToColumn(cardId(e), previousColumnId, columnId, user.getId(), time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/CardUpdate.java b/src/main/java/io/lavagna/service/importexport/CardUpdate.java new file mode 100644 index 000000000..dd30e5fb4 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CardUpdate.java @@ -0,0 +1,46 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.CardService; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class CardUpdate extends AbstractProcessEvent { + + private final CardService cardService; + + CardUpdate(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService, + CardService cardService) { + super(cardRepository, userRepository, cardDataService); + this.cardService = cardService; + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + cardService.updateCard(cardId(e), event.getValueString(), user, time); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/CommentCreate.java b/src/main/java/io/lavagna/service/importexport/CommentCreate.java new file mode 100644 index 000000000..bcd0fd343 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CommentCreate.java @@ -0,0 +1,43 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.CardData; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class CommentCreate extends AbstractProcessEvent { + + CommentCreate(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + CardData cd = cardDataService.createComment(cardId(e), e.getContent(), time, user); + context.getCommentsId().put(event.getDataId(), cd.getId()); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/CommentDelete.java b/src/main/java/io/lavagna/service/importexport/CommentDelete.java new file mode 100644 index 000000000..5e0087f00 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CommentDelete.java @@ -0,0 +1,40 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class CommentDelete extends AbstractProcessEvent { + + CommentDelete(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + cardDataService.deleteComment(context.getCommentsId().get(event.getDataId()), user, time); + } +} diff --git a/src/main/java/io/lavagna/service/importexport/CommentUpdate.java b/src/main/java/io/lavagna/service/importexport/CommentUpdate.java new file mode 100644 index 000000000..4c08b16ef --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/CommentUpdate.java @@ -0,0 +1,40 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class CommentUpdate extends AbstractProcessEvent { + + CommentUpdate(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + cardDataService.updateComment(context.getCommentsId().get(event.getDataId()), e.getContent(), time, user); + } +} diff --git a/src/main/java/io/lavagna/service/importexport/DescriptionCreateUpdate.java b/src/main/java/io/lavagna/service/importexport/DescriptionCreateUpdate.java new file mode 100644 index 000000000..bb0310784 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/DescriptionCreateUpdate.java @@ -0,0 +1,42 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class DescriptionCreateUpdate extends AbstractProcessEvent { + + DescriptionCreateUpdate(CardRepository cardRepository, UserRepository userRepository, + CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + cardDataService.updateDescription(cardId(e), e.getContent(), time, user); + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/FileDelete.java b/src/main/java/io/lavagna/service/importexport/FileDelete.java new file mode 100644 index 000000000..0c18970a8 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/FileDelete.java @@ -0,0 +1,44 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import java.nio.file.Path; +import java.util.Date; + +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +class FileDelete extends AbstractProcessEvent { + + FileDelete(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + Integer cardDataId; + if ((cardDataId = context.getFileId().get(event.getDataId())) != null) { + cardDataService.deleteFile(cardDataId, user, time); + } + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/FileUpload.java b/src/main/java/io/lavagna/service/importexport/FileUpload.java new file mode 100644 index 000000000..2afc56cfe --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/FileUpload.java @@ -0,0 +1,64 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.common.Read; +import io.lavagna.model.CardData; +import io.lavagna.model.CardDataUploadContentInfo; +import io.lavagna.model.Event; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.UserRepository; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; +import java.util.Objects; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +import com.google.gson.reflect.TypeToken; + +class FileUpload extends AbstractProcessEvent { + + FileUpload(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService) { + super(cardRepository, userRepository, cardDataService); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + try { + Path p = Objects.requireNonNull(Read.readFile("files/" + e.getContent(), tempFile)); + CardDataUploadContentInfo fileData = Read.readObject("files/" + e.getContent() + ".json", tempFile, + new TypeToken() { + }); + ImmutablePair res = cardDataService.createFile(event.getValueString(), e.getContent(), + fileData.getSize(), cardId(e), Files.newInputStream(p), fileData.getContentType(), user, time); + if (res.getLeft()) { + context.getFileId().put(event.getDataId(), res.getRight().getId()); + } + Files.delete(p); + } catch (IOException ioe) { + throw new IllegalStateException("error while handling event FILE_UPLOAD for event: " + e, ioe); + } + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/ImportEventProcessor.java b/src/main/java/io/lavagna/service/importexport/ImportEventProcessor.java new file mode 100644 index 000000000..52584e8f5 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/ImportEventProcessor.java @@ -0,0 +1,112 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.CardService; +import io.lavagna.service.EventRepository; +import io.lavagna.service.ImportEvent; +import io.lavagna.service.LabelService; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Date; +import java.util.EnumMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ImportEventProcessor implements ImportEvent { + + private final UserRepository userRepository; + private final Map eventProcessors; + + @Autowired + public ImportEventProcessor(CardRepository cardRepository, UserRepository userRepository, + CardDataService cardDataService, CardService cardService, EventRepository eventRepository, + + LabelService labelService, CardLabelRepository cardLabelRepository, BoardRepository boardRepository) { + + this.userRepository = userRepository; + + Map mapping = new EnumMap<>(EventType.class); + + // + mapping.put(EventType.CARD_CREATE, new CardCreate(cardRepository, userRepository, cardDataService, cardService)); + mapping.put(EventType.CARD_UPDATE, new CardUpdate(cardRepository, userRepository, cardDataService, cardService)); + mapping.put(EventType.CARD_MOVE, new CardMove(cardRepository, userRepository, cardDataService, cardService)); + + AbstractProcessEvent abtProcessor = new CardArchiveBacklogTrash(cardRepository, userRepository, + cardDataService, cardService, eventRepository); + mapping.put(EventType.CARD_ARCHIVE, abtProcessor); + mapping.put(EventType.CARD_BACKLOG, abtProcessor); + mapping.put(EventType.CARD_TRASH, abtProcessor); + + AbstractProcessEvent descProcessor = new DescriptionCreateUpdate(cardRepository, userRepository, + cardDataService); + mapping.put(EventType.DESCRIPTION_CREATE, descProcessor); + mapping.put(EventType.DESCRIPTION_UPDATE, descProcessor); + + mapping.put(EventType.COMMENT_CREATE, new CommentCreate(cardRepository, userRepository, cardDataService)); + mapping.put(EventType.COMMENT_UPDATE, new CommentUpdate(cardRepository, userRepository, cardDataService)); + mapping.put(EventType.COMMENT_DELETE, new CommentDelete(cardRepository, userRepository, cardDataService)); + + mapping.put(EventType.ACTION_LIST_CREATE, new ActionListCreate(cardRepository, userRepository, cardDataService)); + mapping.put(EventType.ACTION_LIST_DELETE, new ActionListDelete(cardRepository, userRepository, cardDataService)); + + mapping.put(EventType.ACTION_ITEM_CREATE, new ActionItemCreate(cardRepository, userRepository, cardDataService)); + mapping.put(EventType.ACTION_ITEM_DELETE, new ActionItemDelete(cardRepository, userRepository, cardDataService)); + mapping.put(EventType.ACTION_ITEM_MOVE, new ActionItemMove(cardRepository, userRepository, cardDataService)); + + AbstractProcessEvent actionItemCheckUncheck = new ActionItemCheckUncheck(cardRepository, userRepository, + cardDataService); + mapping.put(EventType.ACTION_ITEM_CHECK, actionItemCheckUncheck); + mapping.put(EventType.ACTION_ITEM_UNCHECK, actionItemCheckUncheck); + + mapping.put(EventType.LABEL_CREATE, new LabelCreate(cardRepository, userRepository, cardDataService, + labelService, cardLabelRepository, boardRepository, eventRepository)); + mapping.put(EventType.LABEL_DELETE, new LabelDelete(cardRepository, userRepository, cardDataService, + labelService, cardLabelRepository, boardRepository, eventRepository)); + + mapping.put(EventType.FILE_UPLOAD, new FileUpload(cardRepository, userRepository, cardDataService)); + mapping.put(EventType.FILE_DELETE, new FileDelete(cardRepository, userRepository, cardDataService)); + // + eventProcessors = Collections.unmodifiableMap(mapping); + } + + public void processEvent(EventFull e, ImportContext context, Path tempFile) { + Event event = e.getEvent(); + + if (eventProcessors.containsKey(event.getEvent())) { + User user = userRepository.findUserByName(e.getUserProvider(), e.getUsername()); + Date time = event.getTime(); + + eventProcessors.get(event.getEvent()).process(e, event, time, user, context, tempFile); + } + } +} diff --git a/src/main/java/io/lavagna/service/importexport/LabelCreate.java b/src/main/java/io/lavagna/service/importexport/LabelCreate.java new file mode 100644 index 000000000..01ebd7b0a --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/LabelCreate.java @@ -0,0 +1,57 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.EventRepository; +import io.lavagna.service.LabelService; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class LabelCreate extends AbstractProcessLabelEvent { + + LabelCreate(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService, + LabelService labelService, CardLabelRepository cardLabelRepository, BoardRepository boardRepository, + EventRepository eventRepository) { + super(cardRepository, userRepository, cardDataService, labelService, cardLabelRepository, boardRepository, + eventRepository); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + CardLabel cl = findLabelByEvent(e); + LabelValue lv; + if (cl != null && (lv = labelValue(cl, e)) != null) { + labelService.addLabelValueToCard(cl.getId(), cardId(e), lv, user, time); + } else { + insertLabelEvent(e, event, time, EventType.LABEL_CREATE); + } + } + +} diff --git a/src/main/java/io/lavagna/service/importexport/LabelDelete.java b/src/main/java/io/lavagna/service/importexport/LabelDelete.java new file mode 100644 index 000000000..f3ef1ff62 --- /dev/null +++ b/src/main/java/io/lavagna/service/importexport/LabelDelete.java @@ -0,0 +1,54 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.service.importexport; + +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.EventFull; +import io.lavagna.model.ImportContext; +import io.lavagna.model.User; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.EventRepository; +import io.lavagna.service.LabelService; +import io.lavagna.service.UserRepository; + +import java.nio.file.Path; +import java.util.Date; + +class LabelDelete extends AbstractProcessLabelEvent { + + LabelDelete(CardRepository cardRepository, UserRepository userRepository, CardDataService cardDataService, + LabelService labelService, CardLabelRepository cardLabelRepository, BoardRepository boardRepository, + EventRepository eventRepository) { + super(cardRepository, userRepository, cardDataService, labelService, cardLabelRepository, boardRepository, + eventRepository); + } + + @Override + void process(EventFull e, Event event, Date time, User user, ImportContext context, Path tempFile) { + CardLabelValue clv = findCardLabelValueBy(e); + if (clv != null) { + labelService.removeLabelValue(clv, user, time); + } else { + insertLabelEvent(e, event, time, EventType.LABEL_DELETE); + } + } +} diff --git a/src/main/java/io/lavagna/web/api/ApplicationConfigurationController.java b/src/main/java/io/lavagna/web/api/ApplicationConfigurationController.java new file mode 100644 index 000000000..613d82c2c --- /dev/null +++ b/src/main/java/io/lavagna/web/api/ApplicationConfigurationController.java @@ -0,0 +1,118 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import static java.lang.String.format; +import static java.util.EnumSet.of; +import io.lavagna.model.ConfigurationKeyValue; +import io.lavagna.model.Key; +import io.lavagna.model.MailConfig; +import io.lavagna.model.Pair; +import io.lavagna.model.Permission; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.Ldap; +import io.lavagna.web.api.model.Conf; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@ExpectPermission(Permission.ADMINISTRATION) +public class ApplicationConfigurationController { + + private final ConfigurationRepository configurationRepository; + private final Ldap ldap; + + @Autowired + public ApplicationConfigurationController(ConfigurationRepository configurationRepository, Ldap ldap) { + this.configurationRepository = configurationRepository; + this.ldap = ldap; + } + + @RequestMapping(value = "/api/check-https-config", method = RequestMethod.GET) + public List checkHttpsConfiguration(HttpServletRequest req) { + + List status = new ArrayList<>(2); + + Map configuration = configurationRepository.findConfigurationFor(of(Key.USE_HTTPS, + Key.BASE_APPLICATION_URL)); + + final boolean useHttps = "true".equals(configuration.get(Key.USE_HTTPS)); + if (req.getServletContext().getSessionCookieConfig().isSecure() != useHttps) { + status.add("SessionCookieConfig is not aligned with settings. The application must be restarted."); + } + + final String baseApplicationUrl = configuration.get(Key.BASE_APPLICATION_URL); + + if (useHttps && !baseApplicationUrl.startsWith("https://")) { + status.add(format( + "The base application url %s does not begin with https:// . It's a mandatory requirement if you want to enable full https mode.", + baseApplicationUrl)); + } + + return status; + } + + @RequestMapping(value = "/api/application-configuration/", method = RequestMethod.GET) + public Map findAll() { + Map res = new EnumMap<>(Key.class); + for (ConfigurationKeyValue kv : configurationRepository.findAll()) { + res.put(kv.getFirst(), kv.getSecond()); + } + return res; + } + + @RequestMapping(value = "/api/application-configuration/{key}", method = RequestMethod.GET) + public ConfigurationKeyValue findByKey(@PathVariable("key") Key key) { + String value = configurationRepository.hasKeyDefined(key) ? configurationRepository.getValue(key) : null; + return new ConfigurationKeyValue(key, value); + } + + @RequestMapping(value = "/api/application-configuration/{key}", method = RequestMethod.DELETE) + public void deleteByKey(@PathVariable("key") Key key) { + configurationRepository.delete(key); + } + + @RequestMapping(value = "/api/application-configuration/", method = RequestMethod.POST) + public void setKeyValue(@RequestBody Conf conf) { + configurationRepository.updateOrCreate(conf.getToUpdateOrCreate()); + } + + @RequestMapping(value = "/api/check-ldap/", method = RequestMethod.POST) + public Pair> checkLdap(@RequestBody Map r) { + return ldap.authenticateWithParams(r.get("serverUrl"), r.get("managerDn"), r.get("managerPassword"), + r.get("userSearchBase"), r.get("userSearchFilter"), r.get("username"), r.get("password")); + } + + @RequestMapping(value = "/api/check-smtp/", method = RequestMethod.POST) + public void checkSmtp(@RequestBody MailConfig mailConfig, @RequestParam("to") String to) { + mailConfig.send(to, "LAVAGNA: TEST", "TEST"); + } +} diff --git a/src/main/java/io/lavagna/web/api/BoardColumnController.java b/src/main/java/io/lavagna/web/api/BoardColumnController.java new file mode 100644 index 000000000..1c7c06e4b --- /dev/null +++ b/src/main/java/io/lavagna/web/api/BoardColumnController.java @@ -0,0 +1,163 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Board; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.BoardColumnInfo; +import io.lavagna.model.Permission; +import io.lavagna.model.User; +import io.lavagna.service.BoardColumnRepository; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.ProjectService; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.lang3.Validate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BoardColumnController { + + private static final Logger LOG = LogManager.getLogger(); + + private final BoardColumnRepository boardColumnRepository; + private final BoardRepository boardRepository; + private final ProjectService projectService; + private final EventEmitter eventEmitter; + + @Autowired + public BoardColumnController(BoardColumnRepository boardColumnRepository, BoardRepository boardRepository, + ProjectService projectService, EventEmitter eventEmitter) { + this.boardColumnRepository = boardColumnRepository; + this.boardRepository = boardRepository; + this.projectService = projectService; + this.eventEmitter = eventEmitter; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/column/{columnId}", method = RequestMethod.GET) + public BoardColumnInfo getColumnInfo(@PathVariable("columnId") int columnId) { + return boardColumnRepository.getColumnInfoById(columnId); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/board/{shortName}/columns-in/{location}", method = RequestMethod.GET) + public List fetchAll(@PathVariable("shortName") String shortName, + @PathVariable("location") BoardColumnLocation location) { + int boardId = boardRepository.findBoardIdByShortName(shortName); + return boardColumnRepository.findAllColumnsFor(boardId, location); + } + + @ExpectPermission(Permission.CREATE_COLUMN) + @RequestMapping(value = "/api/board/{shortName}/column", method = RequestMethod.POST) + public void create(@PathVariable("shortName") String shortName, @RequestBody BoardColumnToCreate column) { + int boardId = boardRepository.findBoardIdByShortName(shortName); + LOG.debug("column: {}, definition: {}", column.name, column.definition); + + Validate.isTrue(projectService.findRelatedProjectShortNameByBoardShortname(shortName).equals( + projectService.findRelatedProjectShortNameByColumnDefinitionId(column.definition))); + + boardColumnRepository.addColumnToBoard(column.name, column.definition, BoardColumnLocation.BOARD, boardId); + + eventEmitter.emitCreateColumn(shortName, BoardColumnLocation.BOARD); + } + + @ExpectPermission(Permission.RENAME_COLUMN) + @RequestMapping(value = "/api/column/{columnId}/rename/{newName}", method = RequestMethod.POST) + public int rename(@PathVariable("columnId") int columnId, @PathVariable("newName") String newName) { + BoardColumn column = boardColumnRepository.findById(columnId); + Board board = boardRepository.findBoardById(column.getBoardId()); + int res = boardColumnRepository.renameColumn(columnId, newName, column.getBoardId()); + + eventEmitter.emitUpdateColumn(board.getShortName(), boardColumnRepository.findById(columnId).getLocation(), + columnId); + + return res; + } + + @ExpectPermission(Permission.RENAME_COLUMN) + @RequestMapping(value = "/api/column/{columnId}/redefine/{newDefinitionId}", method = RequestMethod.POST) + public int redefine(@PathVariable("columnId") int columnId, @PathVariable("newDefinitionId") int definitionId) { + + Validate.isTrue(projectService.findRelatedProjectShortNameByColumnId(columnId).equals( + projectService.findRelatedProjectShortNameByColumnDefinitionId(definitionId))); + + BoardColumn column = boardColumnRepository.findById(columnId); + Board board = boardRepository.findBoardById(column.getBoardId()); + int res = boardColumnRepository.redefineColumn(columnId, definitionId, column.getBoardId()); + + eventEmitter.emitUpdateColumn(board.getShortName(), boardColumnRepository.findById(columnId).getLocation(), + columnId); + + return res; + } + + @ExpectPermission(Permission.MOVE_COLUMN) + @RequestMapping(value = "/api/board/{shortName}/columns-in/{location}/column/order", method = RequestMethod.POST) + public boolean reorder(@PathVariable("shortName") String shortName, + @PathVariable("location") BoardColumnLocation location, @RequestBody List columnIdOrdered) { + int boardId = boardRepository.findBoardIdByShortName(shortName); + boardColumnRepository.updateColumnOrder(Utils.from(columnIdOrdered), boardId, location); + + eventEmitter.emitUpdateColumnPosition(shortName, location); + + return true; + } + + /** + * Move the column in the given {location} + */ + @ExpectPermission(Permission.MOVE_COLUMN) + @RequestMapping(value = "/api/column/{columnId}/to-location/{location}", method = RequestMethod.POST) + @ResponseBody + public void moveColumnWithoutReorder(@PathVariable("columnId") int columnId, + @PathVariable("location") BoardColumnLocation location, User user) { + Validate.isTrue(location != BoardColumnLocation.BOARD); + BoardColumn col = boardColumnRepository.findById(columnId); + + Validate.isTrue(col.getLocation() == BoardColumnLocation.BOARD); + + boardColumnRepository.moveToLocation(col.getId(), location, user); + + String boardShortName = boardRepository.findBoardById(col.getBoardId()).getShortName(); + eventEmitter.emitUpdateColumnPosition(boardShortName, BoardColumnLocation.BOARD); + eventEmitter.emitMoveCardOutsideOfBoard(boardShortName, location); + // FIXME we should fetch the affected card ids and send a a card moved event + } + + @Getter + @Setter + public static class BoardColumnToCreate { + private String name; + private Integer definition; + } +} diff --git a/src/main/java/io/lavagna/web/api/BoardController.java b/src/main/java/io/lavagna/web/api/BoardController.java new file mode 100644 index 000000000..014b30bef --- /dev/null +++ b/src/main/java/io/lavagna/web/api/BoardController.java @@ -0,0 +1,129 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Board; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.Permission; +import io.lavagna.model.UserWithPermission; +import io.lavagna.model.util.ShortNameGenerator; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.StatisticsService; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.ProjectService; +import io.lavagna.service.SearchService; +import io.lavagna.web.api.model.Suggestion; +import io.lavagna.web.api.model.TaskStatistics; +import io.lavagna.web.api.model.TaskStatisticsAndHistory; +import io.lavagna.web.api.model.UpdateRequest; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.Date; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BoardController { + + private final BoardRepository boardRepository; + private final ProjectService projectService; + private final SearchService searchService; + private final StatisticsService statisticsService; + private final EventEmitter eventEmitter; + + @Autowired + public BoardController(BoardRepository boardRepository, ProjectService projectService, SearchService searchService, + EventEmitter eventEmitter, StatisticsService statisticsService) { + this.boardRepository = boardRepository; + this.projectService = projectService; + this.searchService = searchService; + this.eventEmitter = eventEmitter; + this.statisticsService = statisticsService; + } + + @RequestMapping(value = "/api/suggest-board-short-name", method = RequestMethod.GET) + public Suggestion suggestBoardShortName(@RequestParam("name") String name) { + return new Suggestion(ShortNameGenerator.generateShortNameFrom(name)); + } + + @RequestMapping(value = "/api/check-board-short-name", method = RequestMethod.GET) + public boolean checkBoardShortName(@RequestParam("name") String name) { + return ShortNameGenerator.isShortNameValid(name) && !boardRepository.existsWithShortName(name); + } + + @ExpectPermission(Permission.UPDATE_BOARD) + @RequestMapping(value = "/api/board/{shortName}", method = RequestMethod.POST) + public Board updateBoard(@PathVariable("shortName") String shortName, @RequestBody UpdateRequest updatedBoard) { + Board board = boardRepository.findBoardByShortName(shortName); + board = boardRepository.updateBoard(board.getId(), updatedBoard.getName(), updatedBoard.getDescription(), + updatedBoard.isArchived()); + eventEmitter.emitUpdateBoard(shortName); + return board; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/board/{shortName}", method = RequestMethod.GET) + public Board findByShortName(@PathVariable("shortName") String shortName) { + return boardRepository.findBoardByShortName(shortName); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/board/{shortName}/task-statistics", method = RequestMethod.GET) + public TaskStatistics boardTaskStatistics(@PathVariable("shortName") String shortName, UserWithPermission user) { + Board board = boardRepository.findBoardByShortName(shortName); + + Map tasks = searchService + .findTaksByColumnDefinition(board.getProjectId(), board.getId(), false, user); + + Map columnDefinitions = projectService + .findMappedColumnDefinitionsByProjectId(board.getProjectId()); + + return new TaskStatistics(tasks, columnDefinitions); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/board/{shortName}/statistics/{fromDate}", method = RequestMethod.GET) + public TaskStatisticsAndHistory boardStatistics(@PathVariable("shortName") String shortName, + @PathVariable("fromDate") Date fromDate, UserWithPermission user) { + Board board = boardRepository.findBoardByShortName(shortName); + + Map tasks = searchService + .findTaksByColumnDefinition(board.getProjectId(), board.getId(), false, user); + + Map columnDefinitions = projectService + .findMappedColumnDefinitionsByProjectId(board.getProjectId()); + + Integer activeUsers = statisticsService.getActiveUsersOnBoard(board.getId(), fromDate); + + return new TaskStatisticsAndHistory(tasks, columnDefinitions, + statisticsService.getCardsStatusByBoard(board.getId(), fromDate), + statisticsService.getCreatedAndClosedCardsByBoard(board.getId(), fromDate), + activeUsers, + statisticsService.getAverageUsersPerCardOnBoard(board.getId()), + statisticsService.getAverageCardsPerUserOnBoard(board.getId()), + statisticsService.getCardsByLabelOnBoard(board.getId()), + activeUsers > 0 ? statisticsService.getMostActiveCardByBoard(board.getId(), fromDate) : null); + } +} diff --git a/src/main/java/io/lavagna/web/api/BulkOperationLabelController.java b/src/main/java/io/lavagna/web/api/BulkOperationLabelController.java new file mode 100644 index 000000000..8e70dae74 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/BulkOperationLabelController.java @@ -0,0 +1,139 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Permission; +import io.lavagna.model.User; +import io.lavagna.service.BulkOperationService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.EventEmitter; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@ExpectPermission(Permission.MANAGE_LABEL_VALUE) +public class BulkOperationLabelController { + + private final BulkOperationService bulkOperationService; + private final CardRepository cardRepository; + private final EventEmitter eventEmitter; + + @Autowired + public BulkOperationLabelController(BulkOperationService bulkOperationService, CardRepository cardRepository, + EventEmitter eventEmitter) { + this.bulkOperationService = bulkOperationService; + this.cardRepository = cardRepository; + this.eventEmitter = eventEmitter; + } + + /** + * ASSIGN a user to the cards if it's not present. + * + * @param projectShortName + * @param op + * @param user + */ + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/assign", method = RequestMethod.POST) + public void assign(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + List affected = bulkOperationService.assign(projectShortName, op.cardIds, op.value, user); + eventEmitter.emitAddLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/remove-assign", method = RequestMethod.POST) + public void removeAssign(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + List affected = bulkOperationService.removeAssign(projectShortName, op.cardIds, op.value, user); + eventEmitter.emitRemoveLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/re-assign", method = RequestMethod.POST) + public void reAssign(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + List affected = bulkOperationService.reAssign(projectShortName, op.cardIds, op.value, user); + eventEmitter.emitAddLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/set-due-date", method = RequestMethod.POST) + public void setDueDate(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + ImmutablePair, List> updatedAndAdded = bulkOperationService.setDueDate(projectShortName, + op.cardIds, op.value, user); + eventEmitter.emitUpdateOrAddValueToCards(cardRepository.findAllByIds(updatedAndAdded.getLeft()), + cardRepository.findAllByIds(updatedAndAdded.getRight())); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/remove-due-date", method = RequestMethod.POST) + public void removeDueDate(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + List affected = bulkOperationService.removeDueDate(projectShortName, op.cardIds, user); + eventEmitter.emitRemoveLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/set-milestone", method = RequestMethod.POST) + public void setMilestone(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + ImmutablePair, List> updatedAndAdded = bulkOperationService.setMilestone( + projectShortName, op.cardIds, op.value, user); + eventEmitter.emitUpdateOrAddValueToCards(cardRepository.findAllByIds(updatedAndAdded.getLeft()), + cardRepository.findAllByIds(updatedAndAdded.getRight())); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/remove-milestone", method = RequestMethod.POST) + public void removeMilestone(@PathVariable("projectShortName") String projectShortName, + @RequestBody BulkOperation op, User user) { + List affected = bulkOperationService.removeMilestone(projectShortName, op.cardIds, user); + eventEmitter.emitRemoveLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/add-label", method = RequestMethod.POST) + public void addLabel(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + List affected = bulkOperationService.addLabel(projectShortName, op.labelId, op.value, op.cardIds, user); + eventEmitter.emitRemoveLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + @RequestMapping(value = "/api/project/{projectShortName}/bulk-op/remove-label", method = RequestMethod.POST) + public void removeLabel(@PathVariable("projectShortName") String projectShortName, @RequestBody BulkOperation op, + User user) { + List affected = bulkOperationService.removeLabel(projectShortName, op.labelId, op.cardIds, user); + eventEmitter.emitRemoveLabelValueToCards(cardRepository.findAllByIds(affected)); + } + + + @Getter + @Setter + public static class BulkOperation { + private Integer labelId;// can be null + private LabelValue value;// can be null + private List cardIds; + } + +} diff --git a/src/main/java/io/lavagna/web/api/CardController.java b/src/main/java/io/lavagna/web/api/CardController.java new file mode 100644 index 000000000..59f674a5c --- /dev/null +++ b/src/main/java/io/lavagna/web/api/CardController.java @@ -0,0 +1,354 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import static io.lavagna.service.SearchFilter.FilterType; +import static io.lavagna.service.SearchFilter.ValueType; +import static io.lavagna.service.SearchFilter.filter; +import io.lavagna.model.Board; +import io.lavagna.model.BoardColumn; +import io.lavagna.model.BoardColumn.BoardColumnLocation; +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.Card; +import io.lavagna.model.CardFull; +import io.lavagna.model.CardFullWithCounts; +import io.lavagna.model.CardFullWithCountsHolder; +import io.lavagna.model.CardLabel; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.Event; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.MilestoneCount; +import io.lavagna.model.Pair; +import io.lavagna.model.Permission; +import io.lavagna.model.Project; +import io.lavagna.model.ProjectAndBoard; +import io.lavagna.model.SearchResults; +import io.lavagna.model.User; +import io.lavagna.model.UserWithPermission; +import io.lavagna.service.BoardColumnRepository; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.CardService; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.ProjectService; +import io.lavagna.service.SearchFilter; +import io.lavagna.service.SearchService; +import io.lavagna.service.StatisticsService; +import io.lavagna.web.api.model.MilestoneDetail; +import io.lavagna.web.api.model.MilestoneInfo; +import io.lavagna.web.api.model.Milestones; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CardController { + + private static final int CARDS_PER_PAGE = 20; + private final CardRepository cardRepository; + private final CardService cardService; + private final CardLabelRepository cardLabelRepository; + private final BoardRepository boardRepository; + private final ProjectService projectService; + private final BoardColumnRepository boardColumnRepository; + private final StatisticsService statisticsService; + private final SearchService searchService; + private final EventEmitter eventEmitter; + + @Autowired + public CardController(CardRepository cardRepository, CardService cardService, + CardLabelRepository cardLabelRepository, BoardRepository boardRepository, ProjectService projectService, + BoardColumnRepository boardColumnRepository, StatisticsService statisticsService, + SearchService searchService, EventEmitter eventEmitter) { + this.cardRepository = cardRepository; + this.cardService = cardService; + this.cardLabelRepository = cardLabelRepository; + this.boardRepository = boardRepository; + this.projectService = projectService; + this.boardColumnRepository = boardColumnRepository; + this.statisticsService = statisticsService; + this.searchService = searchService; + this.eventEmitter = eventEmitter; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/column/{columnId}/card", method = RequestMethod.GET) + public List fetchAllInColumn(@PathVariable("columnId") int columnId) { + return cardService.fetchAllInColumn(columnId); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/cards-by-milestone", method = RequestMethod.GET) + public Milestones findCardsByMilestone(@PathVariable("projectShortName") String projectShortName) { + Project project = projectService.findByShortName(projectShortName); + Map milestoneToIndex = new HashMap<>(); + List milestones = new ArrayList<>(); + getMilestones(project.getId(), milestoneToIndex, milestones); + + for (MilestoneCount count : statisticsService.findCardsCountByMilestone(project.getId())) { + MilestoneInfo md = milestones.get(milestoneToIndex.get(count.getMilestoneId())); + md.getCardsCountByStatus().put(count.getColumnDefinition(), count.getCount()); + } + + Map statusColors = new EnumMap<>(ColumnDefinition.class); + for (BoardColumnDefinition cd : projectService.findColumnDefinitionsByProjectId(project.getId())) { + statusColors.put(cd.getValue(), cd.getColor()); + } + + return new Milestones(milestones, statusColors); + } + + private void getMilestones(int projectId, Map milestoneToIndex, List milestones) { + CardLabel label = cardLabelRepository.findLabelByName(projectId, "MILESTONE", CardLabel.LabelDomain.SYSTEM); + List listValues = cardLabelRepository.findListValuesByLabelId(label.getId()); + int foundUnassignedIndex = -1; + int mIndex = 0; + for (LabelListValue milestone : listValues) { + milestones.add(new MilestoneInfo(milestone, new EnumMap(ColumnDefinition.class))); + milestoneToIndex.put(milestone.getId(), mIndex); + if ("Unassigned".equals(milestone.getValue())) { + foundUnassignedIndex = mIndex; + } + mIndex++; + } + if (foundUnassignedIndex < 0) { + LabelListValue unassigned = new LabelListValue(-1, 0, Integer.MAX_VALUE, "Unassigned"); + milestones.add(new MilestoneInfo(unassigned, new EnumMap(ColumnDefinition.class))); + milestoneToIndex.put(null, milestoneToIndex.size()); + } else { + milestoneToIndex.put(null, foundUnassignedIndex); + } + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/cards-by-milestone-detail/{milestone}/{page}", method = RequestMethod.GET) + public MilestoneDetail findCardsByMilestoneDetail(@PathVariable("projectShortName") String projectShortName, + @PathVariable("milestone") String milestone, @PathVariable("page") int page, UserWithPermission user) { + + int projectId = projectService.findByShortName(projectShortName).getId(); + CardLabel label = cardLabelRepository.findLabelByName(projectId, "MILESTONE", CardLabel.LabelDomain.SYSTEM); + List listValues = cardLabelRepository.findListValuesByLabelIdAndValue(label.getId(), milestone); + + SearchFilter filter; + Map> assignedAndClosedCards; + + if (listValues.size() > 0) { + filter = filter(FilterType.MILESTONE, ValueType.STRING, milestone); + assignedAndClosedCards = statisticsService.getAssignedAndClosedCardsByMilestone(listValues.get(0), + DateUtils.addWeeks(DateUtils.truncate(new Date(), Calendar.DATE), -2)); + } else { + filter = filter(FilterType.MILESTONE, ValueType.UNASSIGNED, null); + assignedAndClosedCards = null; + } + + SearchResults cards = searchService.find(Arrays.asList(filter), projectId, null, user, page); + return new MilestoneDetail(cards, assignedAndClosedCards); + } + + /** + * Return the latest 11 card in a given location, ordered by mutation time. (11 so the user can paginate 10 at the + * time and know if there are more). + * + * @param shortName + * @param location + * @param page + * @return + */ + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/board/{shortName}/cards-in/{location}/{page}", method = RequestMethod.GET) + public List fetchPaginatedIn(@PathVariable("shortName") String shortName, + @PathVariable("location") BoardColumnLocation location, @PathVariable("page") int page) { + int boardId = boardRepository.findBoardIdByShortName(shortName); + return cardRepository.fetchPaginatedByBoardIdAndLocation(boardId, location, page); + } + + private void emitCreateCard(int columnId, Card createdCard) { + ProjectAndBoard projectAndBoard = boardRepository.findProjectAndBoardByColumnId(columnId); + eventEmitter.emitCreateCard(projectAndBoard.getProject().getShortName(), projectAndBoard.getBoard() + .getShortName(), columnId, createdCard.getId()); + } + + // TODO: check that columnId is effectively inside the board named shortName + @ExpectPermission(Permission.CREATE_CARD) + @RequestMapping(value = "/api/column/{columnId}/card", method = RequestMethod.POST) + public void create(@PathVariable("columnId") int columnId, @RequestBody CardData card, User user) { + Card createdCard = cardService.createCard(card.name, columnId, new Date(), user); + emitCreateCard(columnId, createdCard); + } + + @ExpectPermission(Permission.CREATE_CARD) + @RequestMapping(value = "/api/column/{columnId}/card-top", method = RequestMethod.POST) + public void createCardFromTop(@PathVariable("columnId") int columnId, @RequestBody CardData card, User user) { + Card createdCard = cardService.createCardFromTop(card.name, columnId, new Date(), user); + emitCreateCard(columnId, createdCard); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}", method = RequestMethod.GET) + public CardFull findCardById(@PathVariable("cardId") int id) { + return cardRepository.findFullBy(id); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card-by-seq/{boardShortName:[A-Z0-9_]+}-{seqNr:[0-9]+}", method = RequestMethod.GET) + public CardFull findCardIdByBoardNameAndSeq(@PathVariable("boardShortName") String boardShortName, + @PathVariable("seqNr") int seqNr) { + return cardRepository.findFullBy(boardShortName, seqNr); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/activity", method = RequestMethod.GET) + public List getCardActivity(@PathVariable("cardId") int id) { + return cardRepository.fetchAllActivityByCardId(id); + } + + @ExpectPermission(Permission.UPDATE_CARD) + @RequestMapping(value = "/api/card/{cardId}", method = RequestMethod.POST) + public void updateCard(@PathVariable("cardId") int id, @RequestBody CardData updateCard, User user) { + cardService.updateCard(id, updateCard.name, user, new Date()); + + Card c = cardRepository.findBy(id); + ProjectAndBoard projectAndBoard = boardRepository.findProjectAndBoardByColumnId(c.getColumnId()); + eventEmitter.emitUpdateCard(projectAndBoard.getProject().getShortName(), projectAndBoard.getBoard() + .getShortName(), c.getColumnId(), id); + } + + @ExpectPermission(Permission.MOVE_CARD) + @RequestMapping(value = "/api/card/{cardId}/from-column/{previousColumnId}/to-column/{newColumnId}", method = RequestMethod.POST) + public Event moveCardToColumn(@PathVariable("cardId") int id, + @PathVariable("previousColumnId") int previousColumnId, @PathVariable("newColumnId") int newColumnId, + @RequestBody ColumnOrders columnOrders, User user) { + + // + BoardColumn prevCol = boardColumnRepository.findById(previousColumnId); + BoardColumn newCol = boardColumnRepository.findById(newColumnId); + Card c = cardRepository.findBy(id); + Validate.isTrue(c.getColumnId() == prevCol.getId(), "card must be inside previous column"); + Validate.isTrue(prevCol.getBoardId() == newCol.getBoardId(), "can only move inside the same board"); + // + + Event event = cardService.moveCardToColumnAndReorder(id,// + previousColumnId, newColumnId, columnOrders.newContainer, user); + + // + eventEmitter.emitUpdateCardPosition(previousColumnId); + eventEmitter.emitUpdateCardPosition(newColumnId); + // + + Board board = boardRepository.findBoardById(prevCol.getBoardId()); + + if (prevCol.getLocation() != BoardColumnLocation.BOARD) { + eventEmitter.emitMoveCardFromOutsideOfBoard(board.getShortName(), prevCol.getLocation()); + } + eventEmitter.emitCardHasMoved(projectService.findRelatedProjectShortNameByBoardShortname(board.getShortName()), + board.getShortName(), Collections.singletonList(id)); + + return event; + } + + @ExpectPermission(Permission.MOVE_CARD) + @RequestMapping(value = "/api/column/{columnId}/order", method = RequestMethod.POST) + public boolean updateCardOrder(@PathVariable("columnId") int columnId, @RequestBody List cardIds) { + cardRepository.updateCardOrder(Utils.from(cardIds), columnId); + eventEmitter.emitUpdateCardPosition(columnId); + return true; + } + + @ExpectPermission(Permission.MOVE_CARD) + @RequestMapping(value = "/api/card/from-column/{previousColumnId}/to-location/{location}", method = RequestMethod.POST) + public void moveCardsToLocation(@PathVariable("previousColumnId") int previousColumnId, + @PathVariable("location") BoardColumnLocation location, @RequestBody CardIds cardIds, User user) { + Validate.isTrue(location != BoardColumnLocation.BOARD); + Validate.isTrue(!cardIds.cardIds.isEmpty()); + BoardColumn col = boardColumnRepository.findById(cardRepository.findBy(cardIds.cardIds.get(0)).getColumnId()); + + // Validate.isTrue(col.getLocation() == BoardColumnLocation.BOARD); + + BoardColumn destination = boardColumnRepository.findDefaultColumnFor(col.getBoardId(), location); + + Validate.isTrue(col.getLocation() != destination.getLocation()); + + cardService.moveCardsToColumn(cardIds.cardIds, previousColumnId, destination.getId(), user.getId(), + BoardColumnLocation.MAPPING.get(location), new Date()); + + eventEmitter.emitUpdateCardPosition(previousColumnId); + + String boardShortName = boardRepository.findBoardById(destination.getBoardId()).getShortName(); + + if (col.getLocation() == BoardColumnLocation.BOARD) { + eventEmitter.emitMoveCardOutsideOfBoard(boardShortName, location); + } else { + eventEmitter.emitMoveCardFromOutsideOfBoard(boardShortName, col.getLocation()); + eventEmitter.emitMoveCardFromOutsideOfBoard(boardShortName, location); + } + + eventEmitter.emitCardHasMoved(projectService.findRelatedProjectShortNameByBoardShortname(boardShortName), + boardShortName, cardIds.cardIds); + } + + @ExpectPermission(Permission.SEARCH) + @RequestMapping(value = "/api/self/cards/{page}", method = RequestMethod.GET) + public CardFullWithCountsHolder getOpenCards(@PathVariable(value = "page") int page, User user) { + return cardService.getAllOpenCards(user, page, CARDS_PER_PAGE); + } + + @ExpectPermission(Permission.SEARCH) + @RequestMapping(value = "/api/self/project/{projectShortName}/cards/{page}", method = RequestMethod.GET) + public CardFullWithCountsHolder getOpenCardsByProjectShortName( + @PathVariable(value = "projectShortName") String shortName, @PathVariable(value = "page") int page, + User user) { + return cardService.getAllOpenCardsByProject(shortName, user, page, CARDS_PER_PAGE); + } + + @Getter + @Setter + public static class CardData { + private String name; + } + + @Getter + @Setter + public static class ColumnOrders { + private List newContainer; + } + + @Getter + @Setter + public static class CardIds { + private List cardIds; + } +} diff --git a/src/main/java/io/lavagna/web/api/CardDataController.java b/src/main/java/io/lavagna/web/api/CardDataController.java new file mode 100644 index 000000000..6b8ad8c3a --- /dev/null +++ b/src/main/java/io/lavagna/web/api/CardDataController.java @@ -0,0 +1,497 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.CardData; +import io.lavagna.model.CardDataFull; +import io.lavagna.model.CardType; +import io.lavagna.model.Event; +import io.lavagna.model.Event.EventType; +import io.lavagna.model.FileDataLight; +import io.lavagna.model.Key; +import io.lavagna.model.Permission; +import io.lavagna.model.User; +import io.lavagna.service.CardDataRepository; +import io.lavagna.service.CardDataService; +import io.lavagna.service.CardRepository; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.EventRepository; +import io.lavagna.web.helper.CardCommentOwnershipChecker; +import io.lavagna.web.helper.ExpectPermission; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; + +@Controller +public class CardDataController { + + private static final Logger LOG = LogManager.getLogger(); + + private final CardDataService cardDataService; + private final CardDataRepository cardDataRepository; + private final CardRepository cardRepository; + private final EventRepository eventRepository; + private final EventEmitter eventEmitter; + private final ConfigurationRepository configurationRepository; + + @Autowired + public CardDataController(CardDataService cardDataService, CardDataRepository cardDataRepository, + CardRepository cardRepository, ConfigurationRepository configurationRepository, + EventRepository eventRepository, EventEmitter eventEmitter) { + this.cardDataService = cardDataService; + this.cardDataRepository = cardDataRepository; + this.cardRepository = cardRepository; + this.eventRepository = eventRepository; + this.eventEmitter = eventEmitter; + this.configurationRepository = configurationRepository; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/data", method = RequestMethod.GET) + @ResponseBody + public List findAllLightByCardId(@PathVariable("cardId") int cardId) { + return cardDataRepository.findAllDataLightByCardId(cardId); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/description", method = RequestMethod.GET) + @ResponseBody + public CardDataHistory description(@PathVariable("cardId") int cardId) { + List descriptions = cardDataService.findDescriptionByCardId(cardId); + // look for duplicates, the event model will keep the entire history of + // the description + CardDataHistory description = null; + for (CardDataFull des : descriptions) { + if (description == null) { + description = new CardDataHistory(des.getId(), des.getContent(), des.getOrder(), des.getUserId(), + des.getTime(), des.getUserId(), des.getTime()); + } + + if (des.getEventType() == EventType.DESCRIPTION_CREATE) { + description.userId = des.getUserId(); + description.time = des.getTime(); + } + if (des.getEventType() == EventType.DESCRIPTION_UPDATE) { + description.updatedCount++; + // never null because the object's fields are always initialized + if (des.getTime().getTime() > description.getUpdateDate().getTime()) { + description.updateUser = des.getUserId(); + description.updateDate = des.getTime(); + } + } + } + return description; + } + + @ExpectPermission(Permission.UPDATE_CARD) + @RequestMapping(value = "/api/card/{cardId}/description", method = RequestMethod.POST) + @ResponseBody + public int updateDescription(@PathVariable("cardId") int cardId, @RequestBody Content content, User user) { + int result = cardDataService.updateDescription(cardId, content.content, new Date(), user); + eventEmitter.emitUpdateDescription(cardRepository.findBy(cardId).getColumnId(), cardId); + return result; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/comments", method = RequestMethod.GET) + @ResponseBody + public List findAllComments(@PathVariable("cardId") int cardId) { + List comments = cardDataService.findAllCommentsByCardId(cardId); + // look for duplicates, the event model will keep the entire history of + // the comment + Map duplicates = new HashMap<>(); + for (CardDataFull comment : comments) { + if (!duplicates.containsKey(comment.getId())) { + CardDataHistory newComment = new CardDataHistory(comment.getId(), comment.getContent(), + comment.getOrder(), comment.getUserId(), comment.getTime(), comment.getUserId(), + comment.getTime()); + duplicates.put(comment.getId(), newComment); + } + CardDataHistory instance = duplicates.get(comment.getId()); + + if (comment.getEventType() == EventType.COMMENT_CREATE) { + instance.userId = comment.getUserId(); + instance.time = comment.getTime(); + } + if (comment.getEventType() == EventType.COMMENT_UPDATE) { + instance.updatedCount++; + // never null because the object's fields are always initialized + if (comment.getTime().getTime() > instance.updateDate.getTime()) { + instance.updateUser = comment.getUserId(); + instance.updateDate = comment.getTime(); + } + } + duplicates.put(comment.getId(), instance); + } + return new ArrayList(duplicates.values()); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/actionlists", method = RequestMethod.GET) + @ResponseBody + public List findAllActionLists(@PathVariable("cardId") int cardId) { + return cardDataService.findAllActionListsAndItemsByCardId(cardId); + } + + @ExpectPermission(Permission.CREATE_CARD_COMMENT) + @RequestMapping(value = "/api/card/{cardId}/comment", method = RequestMethod.POST) + @ResponseBody + public CardData createComment(@PathVariable("cardId") int cardId, @RequestBody Content commentData, User user) { + CardData comment = cardDataService.createComment(cardId, commentData.content, new Date(), user); + eventEmitter.emitCreateComment(cardRepository.findBy(cardId).getColumnId(), cardId); + return comment; + } + + @ExpectPermission(value = Permission.UPDATE_CARD_COMMENT, ownershipChecker = CardCommentOwnershipChecker.class) + @RequestMapping(value = "/api/card-data/comment/{commentId}", method = RequestMethod.POST) + @ResponseBody + public void updateComment(@PathVariable("commentId") int commentId, @RequestBody Content content, User user) { + cardDataService.updateComment(commentId, content.content, new Date(), user); + eventEmitter.emitUpdateComment(cardDataRepository.getUndeletedDataLightById(commentId).getCardId()); + } + + @ExpectPermission(value = Permission.DELETE_CARD_COMMENT, ownershipChecker = CardCommentOwnershipChecker.class) + @RequestMapping(value = "/api/card-data/comment/{commentId}", method = RequestMethod.DELETE) + @ResponseBody + public Event deleteComment(@PathVariable("commentId") int commentId, User user) { + Event res = cardDataService.deleteComment(commentId, user, new Date()); + eventEmitter.emitDeleteComment(cardRepository.findBy(res.getCardId()).getColumnId(), res.getCardId()); + return res; + } + + @ExpectPermission(value = Permission.DELETE_CARD_COMMENT, ownershipChecker = CardCommentOwnershipChecker.class) + @RequestMapping(value = "/api/card-data/undo/{eventId}/comment", method = RequestMethod.POST) + @ResponseBody + public int undoDeleteComment(@PathVariable("eventId") int eventId, User user) { + Event event = eventRepository.getEventById(eventId); + Validate.isTrue(event.getEvent() == EventType.COMMENT_DELETE); + + cardDataService.undoDeleteComment(event); + eventEmitter.emitUndoDeleteComment(cardRepository.findBy(event.getCardId()).getColumnId(), event.getCardId()); + + return event.getDataId(); + } + + @ExpectPermission(Permission.CREATE_ACTION_LIST) + @RequestMapping(value = "/api/card/{cardId}/actionlist", method = RequestMethod.POST) + @ResponseBody + public CardData createActionList(@PathVariable("cardId") int cardId, @RequestBody Content actionListData, User user) { + CardData actionList = cardDataService.createActionList(cardId, actionListData.content, user, new Date()); + eventEmitter.emitCreateActionList(cardId); + return actionList; + } + + @ExpectPermission(Permission.DELETE_ACTION_LIST) + @RequestMapping(value = "/api/card-data/actionlist/{actionListId}", method = RequestMethod.DELETE) + @ResponseBody + public Event deleteActionList(@PathVariable("actionListId") int actionListId, User user) { + Event res = cardDataService.deleteActionList(actionListId, user, new Date()); + eventEmitter.emitDeleteActionList(cardRepository.findBy(res.getCardId()).getColumnId(), res.getCardId()); + return res; + } + + @ExpectPermission(value = Permission.DELETE_ACTION_LIST) + @RequestMapping(value = "/api/card-data/undo/{eventId}/actionlist", method = RequestMethod.POST) + @ResponseBody + public int undoDeleteActionList(@PathVariable("eventId") int eventId, User user) { + Event event = eventRepository.getEventById(eventId); + Validate.isTrue(event.getEvent() == EventType.ACTION_LIST_DELETE); + + cardDataService.undoDeleteActionList(event); + eventEmitter + .emitUndoDeleteActionList(cardRepository.findBy(event.getCardId()).getColumnId(), event.getCardId()); + + return event.getDataId(); + } + + @ExpectPermission(Permission.UPDATE_ACTION_LIST) + @RequestMapping(value = "/api/card-data/actionlist/{actionListId}/update", method = RequestMethod.POST) + @ResponseBody + public int updateActionList(@PathVariable("actionListId") int actionListId, @RequestBody Content data, User user) { + int res = cardDataService.updateActionList(actionListId, data.content); + eventEmitter.emitUpdateActionList(cardDataRepository.getUndeletedDataLightById(actionListId).getCardId()); + return res; + } + + @ExpectPermission(Permission.CREATE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/actionlist/{actionListId}/item", method = RequestMethod.POST) + @ResponseBody + public CardData createActionItem(@PathVariable("actionListId") int actionListId, + @RequestBody Content actionItemData, User user) { + int cardId = cardDataRepository.getUndeletedDataLightById(actionListId).getCardId(); + CardData actionItem = cardDataService.createActionItem(cardId, actionListId, actionItemData.content, user, + new Date()); + eventEmitter.emitCreateActionItem(cardRepository.findBy(cardId).getColumnId(), cardId); + return actionItem; + } + + @ExpectPermission(Permission.DELETE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/actionitem/{actionItemId}", method = RequestMethod.DELETE) + @ResponseBody + public Event deleteActionItem(@PathVariable("actionItemId") int actionItemId, User user) { + int cardId = cardDataRepository.getUndeletedDataLightById(actionItemId).getCardId(); + Event res = cardDataService.deleteActionItem(actionItemId, user, new Date()); + eventEmitter.emitDeleteActionItem(cardRepository.findBy(cardId).getColumnId(), cardId); + return res; + } + + @ExpectPermission(Permission.DELETE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/undo/{eventId}/actionitem", method = RequestMethod.POST) + @ResponseBody + public int undoDeleteActionItem(@PathVariable("eventId") int eventId, User user) { + Event event = eventRepository.getEventById(eventId); + Validate.isTrue(event.getEvent() == EventType.ACTION_ITEM_DELETE); + + cardDataService.undoDeleteActionItem(event); + eventEmitter.emiteUndoDeleteActionItem(cardRepository.findBy(event.getCardId()).getColumnId(), + event.getCardId()); + + return event.getDataId(); + } + + @ExpectPermission(Permission.TOGGLE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/actionitem/{actionItemId}/toggle/{status}", method = RequestMethod.POST) + @ResponseBody + public int toggleActionItem(@PathVariable("actionItemId") int actionItemId, @PathVariable("status") Boolean status, + User user) { + int cardId = cardDataRepository.getUndeletedDataLightById(actionItemId).getCardId(); + int res = cardDataService.toggleActionItem(actionItemId, status, user, new Date()); + eventEmitter.emitToggleActionItem(cardRepository.findBy(cardId).getColumnId(), cardId); + return res; + } + + @ExpectPermission(Permission.UPDATE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/actionitem/{actionItemId}/update", method = RequestMethod.POST) + @ResponseBody + public int updateActionItem(@PathVariable("actionItemId") int actionItemId, @RequestBody Content data, User user) { + int cardId = cardDataRepository.getUndeletedDataLightById(actionItemId).getCardId(); + int res = cardDataService.updateActionItem(actionItemId, data.content); + eventEmitter.emitUpdateUpdateActionItem(cardId); + return res; + } + + @ExpectPermission(Permission.MOVE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/actionitem/{actionItemId}/move-to-actionlist/{to}", method = RequestMethod.POST) + @ResponseBody + public boolean moveActionItem(@PathVariable("actionItemId") int actionItemId, + @PathVariable("to") Integer newReferenceId, @RequestBody OrderData dataOrder, User user) { + int cardId = cardDataRepository.getUndeletedDataLightById(actionItemId).getCardId(); + + cardDataService.moveActionItem(cardId, actionItemId, newReferenceId, dataOrder.newContainer, user, new Date()); + eventEmitter.emitMoveActionItem(cardId); + return true; + } + + @ExpectPermission(Permission.ORDER_ACTION_LIST) + @RequestMapping(value = "/api/card/{cardId}/order/actionlist", method = RequestMethod.POST) + @ResponseBody + public boolean reorderActionLists(@PathVariable("cardId") int cardId, @RequestBody List order) { + cardDataRepository.updateActionListOrder(cardId, Utils.from(order)); + eventEmitter.emitReorderActionLists(cardId); + return true; + } + + @ExpectPermission(Permission.MOVE_ACTION_LIST_ITEM) + @RequestMapping(value = "/api/card-data/actionlist/{actionListId}/order", method = RequestMethod.POST) + @ResponseBody + public boolean reorderActionItems(@PathVariable("actionListId") int actionListId, @RequestBody List order) { + CardData cd = cardDataRepository.getUndeletedDataLightById(actionListId); + Validate.isTrue(cd.getType() == CardType.ACTION_LIST); + int cardId = cd.getCardId(); + cardDataRepository.updateOrderByCardAndReferenceId(cardId, actionListId, Utils.from(order)); + eventEmitter.emitReorderActionItems(cardId); + return true; + } + + @ExpectPermission(Permission.CREATE_FILE) + @RequestMapping(value = "/api/card/{cardId}/file", method = RequestMethod.POST) + @ResponseBody + public List uploadFiles(@PathVariable("cardId") int cardId, + @RequestParam("files") List files, User user, HttpServletResponse resp) throws IOException { + + LOG.debug("Files uploaded: {}", files.size()); + + if (!ensureFileSize(files)) { + resp.setStatus(422); + return Collections.emptyList(); + } + + List digests = new ArrayList<>(); + for (MultipartFile file : files) { + Path p = Files.createTempFile("lavagna", "upload"); + try (InputStream fileIs = file.getInputStream()) { + Files.copy(fileIs, p, StandardCopyOption.REPLACE_EXISTING); + + String digest = DigestUtils.sha256Hex(Files.newInputStream(p)); + boolean result = cardDataService.createFile(file.getOriginalFilename(), digest, file.getSize(), cardId, + Files.newInputStream(p), file.getContentType(), user, new Date()).getLeft(); + if (result) { + LOG.debug("file uploaded! size: {}, original name: {}, content-type: {}", file.getSize(), + file.getOriginalFilename(), file.getContentType()); + digests.add(digest); + } + } finally { + Files.delete(p); + LOG.debug("deleted temp file {}", p); + } + } + eventEmitter.emitUploadFile(cardRepository.findBy(cardId).getColumnId(), cardId); + return digests; + } + + private boolean ensureFileSize(List files) { + Integer maxSizeInByte = NumberUtils.createInteger(configurationRepository + .getValueOrNull(Key.MAX_UPLOAD_FILE_SIZE)); + if (maxSizeInByte == null) { + return true; + } + for (MultipartFile file : files) { + if (file.getSize() > maxSizeInByte) { + return false; + } + } + return true; + } + + // TODO: fix exception handling + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card-data/file/{fileId}", method = RequestMethod.GET) + public void getFile(@PathVariable("fileId") int fileId, HttpServletResponse response) { + FileDataLight fileData = cardDataRepository.getUndeletedFileByCardDataId(fileId); + response.addHeader("Content-Disposition", "attachment;filename=\"" + fileData.getName() + "\""); + try (OutputStream out = response.getOutputStream()) { + cardDataRepository.outputFileContent(fileData.getDigest(), out); + response.setContentType(fileData.getContentType()); + } catch (IOException e) { + LOG.error("error getting file", e); + response.setStatus(500); + } + } + + @ExpectPermission(Permission.DELETE_FILE) + @RequestMapping(value = "/api/card-data/file/{fileId}", method = RequestMethod.DELETE) + @ResponseBody + public Event deleteFile(@PathVariable("fileId") int fileId, User user) { + Event result = cardDataService.deleteFile(fileId, user, new Date()); + eventEmitter.emitDeleteFile(cardRepository.findBy(result.getCardId()).getColumnId(), result.getCardId()); + return result; + } + + @ExpectPermission(Permission.DELETE_FILE) + @RequestMapping(value = "/api/card-data/undo/{eventId}/file", method = RequestMethod.POST) + @ResponseBody + public int undoDeleteFile(@PathVariable("eventId") int eventId, User user) { + Event event = eventRepository.getEventById(eventId); + Validate.isTrue(event.getEvent() == EventType.FILE_DELETE); + + cardDataService.undoDeleteFile(event); + eventEmitter.emiteUndoDeleteFile(cardRepository.findBy(event.getCardId()).getColumnId(), event.getCardId()); + + return event.getDataId(); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/files", method = RequestMethod.GET) + @ResponseBody + public List findAllFiles(@PathVariable("cardId") int cardId) { + return cardDataRepository.findAllFilesByCardId(cardId); + } + + // -- activity extension + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card-data/activity/{id}", method = RequestMethod.GET) + @ResponseBody + public CardData getCardDataById(@PathVariable("id") int id) { + return cardDataRepository.getUndeletedDataLightById(id); + } + + @Getter + @Setter + public static class Content { + private String content; + } + + @Getter + @Setter + public static class OrderData { + private List newContainer; + } + + @Getter + @Setter + public static class FileUpload { + private MultipartFile file; + } + + @Getter + @Setter + public static class CardDataHistory { + private final int id; + private int userId; + private Date time; + private final String content; + private int updatedCount = 0; + private int updateUser; + private Date updateDate; + private final int order; + + public CardDataHistory(int id, String content, int order, int createUser, Date createDate, int updateUser, + Date updatedDate) { + this.id = id; + this.content = content; + this.order = order; + + this.userId = createUser; + this.time = createDate; + + this.updateUser = updateUser; + this.updateDate = updatedDate; + } + } + +} diff --git a/src/main/java/io/lavagna/web/api/CardLabelController.java b/src/main/java/io/lavagna/web/api/CardLabelController.java new file mode 100644 index 000000000..ff02e2f95 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/CardLabelController.java @@ -0,0 +1,264 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.CardFull; +import io.lavagna.model.CardLabel; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.CardLabelValue; +import io.lavagna.model.CardLabelValue.LabelValue; +import io.lavagna.model.Event; +import io.lavagna.model.Label; +import io.lavagna.model.LabelListValue; +import io.lavagna.model.Permission; +import io.lavagna.model.Project; +import io.lavagna.model.User; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.LabelService; +import io.lavagna.service.ProjectService; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CardLabelController { + + private final CardRepository cardRepository; + private final LabelService labelService; + private final CardLabelRepository cardLabelRepository; + private final EventEmitter eventEmitter; + private final ProjectService projectService; + + @Autowired + public CardLabelController(CardRepository cardRepository, ProjectService projectService, LabelService labelService, + CardLabelRepository cardLabelRepository, EventEmitter eventEmitter) { + this.cardRepository = cardRepository; + this.labelService = labelService; + this.cardLabelRepository = cardLabelRepository; + this.eventEmitter = eventEmitter; + this.projectService = projectService; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/labels", method = RequestMethod.GET) + public Map findLabelsByProjectId(@PathVariable("projectShortName") String projectShortName) { + Map res = new TreeMap<>(); + Project project = projectService.findByShortName(projectShortName); + for (CardLabel cl : cardLabelRepository.findLabelsByProject(project.getId())) { + res.put(cl.getId(), cl); + } + return res; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/card/{cardId}/label-values", method = RequestMethod.GET) + public Map> findCardLabelValuesByCardId(@PathVariable("cardId") int cardId) { + return from(cardLabelRepository.findCardLabelValuesByCardId(cardId)); + } + + @ExpectPermission(Permission.CREATE_LABEL) + @RequestMapping(value = "/api/project/{projectShortName}/labels", method = RequestMethod.POST) + public CardLabel addLabel(@PathVariable("projectShortName") String projectShortName, @RequestBody Label label) { + Project project = projectService.findByShortName(projectShortName); + CardLabel cl = cardLabelRepository.addLabel(project.getId(), label.isUnique(), label.getType(), + LabelDomain.USER, label.getName(), label.getColor()); + eventEmitter.emitAddLabel(project.getShortName()); + return cl; + } + + @ExpectPermission(Permission.PROJECT_ADMINISTRATION) + @RequestMapping(value = "/api/label/{labelId}/use-count", method = RequestMethod.GET) + public int labelUseCount(@PathVariable("labelId") int labelId) { + return cardLabelRepository.labelUsedCount(labelId); + } + + @ExpectPermission(Permission.UPDATE_LABEL) + @RequestMapping(value = "/api/label/{labelId}", method = RequestMethod.POST) + public void updateLabel(@PathVariable("labelId") int labelId, @RequestBody Label label) { + CardLabel cl = cardLabelRepository.updateLabel(labelId, label); + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitUpdateLabel(project.getShortName(), labelId); + } + + @ExpectPermission(Permission.PROJECT_ADMINISTRATION) + @RequestMapping(value = "/api/system-label/{labelId}", method = RequestMethod.POST) + public void updateSystemLabel(@PathVariable("labelId") int labelId, @RequestBody Label label) { + CardLabel cl = cardLabelRepository.updateSystemLabel(labelId, label); + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitUpdateLabel(project.getShortName(), labelId); + } + + @ExpectPermission(Permission.DELETE_LABEL) + @RequestMapping(value = "/api/label/{labelId}", method = RequestMethod.DELETE) + public void removeLabel(@PathVariable("labelId") int labelId) { + + CardLabel cl = cardLabelRepository.findLabelById(labelId); + cardLabelRepository.removeLabel(labelId); + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitDeleteLabel(project.getShortName(), labelId); + } + + @ExpectPermission(Permission.MANAGE_LABEL_VALUE) + @RequestMapping(value = "/api/card/{cardId}/label-value", method = RequestMethod.POST) + public void addLabelValueToCard(@PathVariable("cardId") int cardId, + @RequestBody LabelIdAndLabelValue labelIdAndLabelValue, User user) { + + // ensure that the project from the card id is the same as the one from the label id + Validate.isTrue(projectService.findRelatedProjectShortNameByCardId(cardId).equals( + projectService.findRelatedProjectShortNameByLabelId(labelIdAndLabelValue.labelId))); + + labelService.addLabelValueToCard(labelIdAndLabelValue.labelId, cardId, labelIdAndLabelValue.labelValue, user, + new Date()); + + CardFull card = cardRepository.findFullBy(cardId); + eventEmitter.emitAddLabelValueToCard(card.getProjectShortName(), card.getColumnId(), cardId); + } + + @Getter + @Setter + public static class LabelIdAndLabelValue { + private int labelId; + private LabelValue labelValue; + } + + @ExpectPermission(Permission.MANAGE_LABEL_VALUE) + @RequestMapping(value = "/api/card-label-value/{labelValueId}", method = RequestMethod.POST) + public void updateLabelValue(@PathVariable("labelValueId") int labelValueId, @RequestBody LabelValue labelValue, + User user) { + + CardLabelValue cardLabelValue = cardLabelRepository.findLabelValueById(labelValueId); + CardLabel cl = cardLabelRepository.findLabelById(cardLabelValue.getLabelId()); + + labelService.updateLabelValue(cardLabelValue.newValue(cl.getType(), labelValue), user, new Date()); + + CardFull card = cardRepository.findFullBy(cardLabelValue.getCardId()); + eventEmitter.emitUpdateLabelValue(card.getProjectShortName(), card.getColumnId(), cardLabelValue.getCardId()); + } + + @ExpectPermission(Permission.MANAGE_LABEL_VALUE) + @RequestMapping(value = "/api/card-label-value/{labelValueId}", method = RequestMethod.DELETE) + public Event removeLabelValue(@PathVariable("labelValueId") int labelValueId, User user) { + + CardLabelValue cardLabelValue = cardLabelRepository.findLabelValueById(labelValueId); + + Event res = labelService.removeLabelValue(cardLabelValue, user, new Date()); + + CardFull card = cardRepository.findFullBy(cardLabelValue.getCardId()); + eventEmitter.emitRemoveLabelValue(card.getProjectShortName(), card.getColumnId(), cardLabelValue.getCardId()); + + return res; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/label/{labelId}/label-list-values", method = RequestMethod.GET) + public List findLabelListValues(@PathVariable("labelId") int labelId) { + return cardLabelRepository.findListValuesByLabelId(labelId); + } + + @ExpectPermission(Permission.UPDATE_LABEL) + @RequestMapping(value = "/api/label/{labelId}/label-list-values", method = RequestMethod.POST) + public void addLabelListValue(@PathVariable("labelId") int labelId, @RequestBody ListValue labelListValue) { + + cardLabelRepository.addLabelListValue(labelId, labelListValue.value); + + CardLabel cl = cardLabelRepository.findLabelById(labelId); + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitUpdateLabel(project.getShortName(), labelId); + } + + @ExpectPermission(Permission.UPDATE_LABEL) + @RequestMapping(value = "/api/label-list-values/{labelListValueId}", method = RequestMethod.DELETE) + public void removeLabelListValue(@PathVariable("labelListValueId") int labelListValueId) { + + LabelListValue labelListValue = cardLabelRepository.findListValueById(labelListValueId); + cardLabelRepository.removeLabelListValue(labelListValueId); + + CardLabel cl = cardLabelRepository.findLabelById(labelListValue.getCardLabelId()); + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitUpdateLabel(project.getShortName(), labelListValue.getCardLabelId()); + } + + @ExpectPermission(Permission.UPDATE_LABEL) + @RequestMapping(value = "/api/label-list-values/{labelListValueId}", method = RequestMethod.POST) + public void updateLabelListValue(@PathVariable("labelListValueId") int labelListValueId, + @RequestBody LabelListValue newLabelListValue) { + + LabelListValue labelListValue = cardLabelRepository.findListValueById(labelListValueId); + + cardLabelRepository.updateLabelListValue(labelListValue.newValue(newLabelListValue.getValue())); + + CardLabel cl = cardLabelRepository.findLabelById(labelListValue.getCardLabelId()); + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitUpdateLabel(project.getShortName(), labelListValue.getCardLabelId()); + } + + @ExpectPermission(Permission.UPDATE_LABEL) + @RequestMapping(value = "/api/label/{labelId}/label-list-values/swap", method = RequestMethod.POST) + public void swapLabelListValues(@PathVariable("labelId") int labelId, @RequestBody SwapListValue swapListValue) { + + LabelListValue llv1 = cardLabelRepository.findListValueById(swapListValue.first); + LabelListValue llv2 = cardLabelRepository.findListValueById(swapListValue.second); + CardLabel cl = cardLabelRepository.findLabelById(labelId); + + // + Validate.isTrue(cl.getId() == llv1.getCardLabelId() && llv1.getCardLabelId() == llv2.getCardLabelId()); + // + + cardLabelRepository.swapLabelListValues(swapListValue.first, swapListValue.second); + + Project project = projectService.findById(cl.getProjectId()); + eventEmitter.emitUpdateLabel(project.getShortName(), labelId); + } + + @Getter + @Setter + public static class ListValue { + private String value; + } + + @Getter + @Setter + public static class SwapListValue { + private int first; + private int second; + } + + private static Map> from(Map> from) { + Map> res = new TreeMap<>(); + for (Entry> kv : from.entrySet()) { + res.put(kv.getKey().getId(), kv.getValue()); + } + return res; + } +} diff --git a/src/main/java/io/lavagna/web/api/ConfigurationController.java b/src/main/java/io/lavagna/web/api/ConfigurationController.java new file mode 100644 index 000000000..7e48103e1 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/ConfigurationController.java @@ -0,0 +1,50 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Key; +import io.lavagna.model.Permission; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.web.helper.ExpectPermission; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Expose some configuration variables that must be visible to all users. + */ +@RestController +@ExpectPermission(Permission.READ) +public class ConfigurationController { + + private final ConfigurationRepository configurationRepository; + + @Autowired + public ConfigurationController(ConfigurationRepository configurationRepository) { + this.configurationRepository = configurationRepository; + } + + @RequestMapping(value = "/api/configuration/max-upload-file-size", method = RequestMethod.GET) + @ResponseBody + public String getMaxUploadFileSize() { + return configurationRepository.getValueOrNull(Key.MAX_UPLOAD_FILE_SIZE); + } + +} diff --git a/src/main/java/io/lavagna/web/api/EndpointInfoController.java b/src/main/java/io/lavagna/web/api/EndpointInfoController.java new file mode 100644 index 000000000..89dddcc97 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/EndpointInfoController.java @@ -0,0 +1,146 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Permission; +import io.lavagna.web.helper.AbstractPermissionUrlPath; +import io.lavagna.web.helper.ExpectPermission; +import io.lavagna.web.helper.PermissionMethodInterceptor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Getter; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +@ExpectPermission(Permission.ADMINISTRATION) +@RestController +public class EndpointInfoController { + + private final RequestMappingHandlerMapping handlerMapping; + + @Autowired + public EndpointInfoController(RequestMappingHandlerMapping handlerMapping) { + this.handlerMapping = handlerMapping; + } + + @RequestMapping(value = "/api/admin/endpoint-info", method = RequestMethod.GET) + public EndpointsInfo getAllEndpoints() { + List res = new ArrayList<>(); + Set pathVariables = new TreeSet<>(); + for (Entry kv : handlerMapping.getHandlerMethods().entrySet()) { + + for (String p : kv.getKey().getPatternsCondition().getPatterns()) { + pathVariables.addAll(extractPathVariables(p)); + } + res.add(new EndpointInfo(kv)); + } + + Collections.sort(res); + + return new EndpointsInfo(pathVariables, res); + } + + private static Set extractPathVariables(String pattern) { + + Set identifierAndPathVariable = new HashSet<>(); + // match stuff like "/project/{projectShortName}" + Pattern p = Pattern.compile("(/[^/]+/\\{[^\\}]+\\})"); + Matcher m = p.matcher(pattern); + while (m.find()) { + identifierAndPathVariable.add(m.group()); + } + + return identifierAndPathVariable; + } + + @Getter + public static class EndpointsInfo { + private final Set idsPathVariable; + private final List endpointsInfo; + private final Map matchedUrlPath = new HashMap<>(); + + public EndpointsInfo(Set idsPathVariable, List endpointsInfo) { + this.idsPathVariable = idsPathVariable; + this.endpointsInfo = endpointsInfo; + for (AbstractPermissionUrlPath p : PermissionMethodInterceptor.URL_PATTERNS_TO_CHECK) { + matchedUrlPath.put(p.getPath(), p.getPath()); + } + } + } + + @Getter + public static class EndpointInfo implements Comparable { + private final Set patterns; + + /** can be null */ + private final Permission permission; + + private final Set methods; + + private final String handler; + + public EndpointInfo(Entry kv) { + patterns = kv.getKey().getPatternsCondition().getPatterns(); + ExpectPermission annotation = ExpectPermission.Helper.getAnnotation(kv.getValue()); + permission = annotation != null ? annotation.value() : null; + methods = kv.getKey().getMethodsCondition().getMethods(); + handler = kv.getValue().getBeanType().getCanonicalName() + "." + kv.getValue().getMethod().getName(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof EndpointInfo)) { + return false; + } + return compareTo((EndpointInfo) obj) == 0; + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(patterns.isEmpty() ? null : patterns.iterator().next()) + .append(methods.isEmpty() ? null : methods.iterator().next()).toHashCode(); + } + + @Override + public int compareTo(EndpointInfo o) { + return new CompareToBuilder() + .append(patterns.isEmpty() ? null : patterns.iterator().next(), + o.patterns.isEmpty() ? null : o.patterns.iterator().next()) + .append(methods.isEmpty() ? null : methods.iterator().next(), + o.methods.isEmpty() ? null : o.methods.iterator().next()).toComparison(); + } + } +} diff --git a/src/main/java/io/lavagna/web/api/ExportImportController.java b/src/main/java/io/lavagna/web/api/ExportImportController.java new file mode 100644 index 000000000..b2ef46420 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/ExportImportController.java @@ -0,0 +1,103 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Permission; +import io.lavagna.model.User; +import io.lavagna.service.ExportImportService; +import io.lavagna.service.ImportService; +import io.lavagna.service.ImportService.TrelloBoardsResponse; +import io.lavagna.service.ImportService.TrelloImportResponse; +import io.lavagna.web.api.model.TrelloImportRequest; +import io.lavagna.web.api.model.TrelloRequest; +import io.lavagna.web.helper.ExpectPermission; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; + +@ExpectPermission(Permission.ADMINISTRATION) +@Controller +public class ExportImportController { + + private final ExportImportService exportImportService; + private final ImportService importService; + + @Autowired + public ExportImportController(ExportImportService exportImportService, ImportService importService) { + this.exportImportService = exportImportService; + this.importService = importService; + } + + @RequestMapping(value = "/api/export", method = RequestMethod.POST) + public void export(HttpServletResponse resp) throws IOException { + resp.setHeader("Content-Disposition", "attachment; filename=\"lavagna-export-" + + new SimpleDateFormat("YYYY-MM-dd").format(new Date()) + ".zip\""); + resp.setContentType("application/octet-stream"); + exportImportService.exportData(resp.getOutputStream()); + } + + @RequestMapping(value = "/api/import/lavagna", method = RequestMethod.POST) + @ResponseBody + public void importFromLavagna( + @RequestParam(value = "overrideConfiguration", defaultValue = "false") Boolean overrideConfiguration, + @RequestParam("file") MultipartFile file) throws IOException { + + Path tempFile = Files.createTempFile(null, null); + + try { + try (InputStream is = file.getInputStream(); OutputStream os = Files.newOutputStream(tempFile)) { + StreamUtils.copy(is, os); + } + + exportImportService.importData(overrideConfiguration, tempFile); + } finally { + Files.delete(tempFile); + } + + } + + @RequestMapping(value = "/api/import/trello/boards", method = RequestMethod.POST) + @ResponseBody + public TrelloBoardsResponse getAvailableTrelloBoards(@RequestBody TrelloRequest request) { + return importService.getAvailableTrelloBoards(request); + } + + @RequestMapping(value = "/api/import/trello", method = RequestMethod.POST) + @ResponseBody + public void importFromTrello(@RequestBody TrelloImportRequest importRequest, User user) { + TrelloImportResponse result = importService.importFromTrello(importRequest); + importService.saveTrelloBoardsToDb(importRequest.getProjectShortName(), result, user); + } + +} diff --git a/src/main/java/io/lavagna/web/api/PermissionController.java b/src/main/java/io/lavagna/web/api/PermissionController.java new file mode 100644 index 000000000..f5c1d1037 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/PermissionController.java @@ -0,0 +1,121 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Permission; +import io.lavagna.model.PermissionCategory; +import io.lavagna.model.Role; +import io.lavagna.model.RoleAndMetadata; +import io.lavagna.model.User; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.PermissionService; +import io.lavagna.service.PermissionService.RoleAndPermissions; +import io.lavagna.web.api.model.CreateRole; +import io.lavagna.web.api.model.UpdateRole; +import io.lavagna.web.api.model.Users; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@ExpectPermission(Permission.ADMINISTRATION) +public class PermissionController { + + private final PermissionService permissionService; + private final EventEmitter eventEmitter; + + @Autowired + public PermissionController(PermissionService permissionService, EventEmitter eventEmitter) { + this.permissionService = permissionService; + this.eventEmitter = eventEmitter; + } + + /** + * @return a map roleName => list of permission + */ + @RequestMapping(value = "/api/role", method = RequestMethod.GET) + public Map findAllRolesAndRelatedPermissions() { + return permissionService.findAllRolesAndRelatedPermission(); + } + + @RequestMapping(value = "/api/role", method = RequestMethod.POST) + public int createRole(@RequestBody CreateRole newRole) { + int res = permissionService.createRole(new Role(newRole.getName())); + eventEmitter.emitCreateRole(); + return res; + } + + @RequestMapping(value = "/api/role/{roleName}", method = RequestMethod.POST) + public void updateRole(@PathVariable("roleName") String roleName, @RequestBody UpdateRole updateRole) { + + RoleAndMetadata role = permissionService.findRoleByName(roleName); + Validate.isTrue(!role.isReadOnly()); + + permissionService.updatePermissionsToRole(new Role(roleName), updateRole.getPermissions()); + eventEmitter.emitUpdatePermissionsToRole(); + } + + @RequestMapping(value = "/api/role/{roleName}", method = RequestMethod.DELETE) + public void deleteRole(@PathVariable("roleName") String roleName) { + + RoleAndMetadata role = permissionService.findRoleByName(roleName); + Validate.isTrue(role.isRemovable()); + + permissionService.deleteRole(new Role(roleName)); + eventEmitter.emitDeleteRole(); + } + + @RequestMapping(value = "/api/role/{roleName}/users/", method = RequestMethod.GET) + public List findUserByRole(@PathVariable("roleName") String roleName) { + return permissionService.findUserByRole(new Role(roleName)); + } + + @RequestMapping(value = "/api/role/{roleName}/users/", method = RequestMethod.POST) + public void assignUsersToRole(@PathVariable("roleName") String roleName, @RequestBody Users usersToAdd) { + permissionService.assignRoleToUsers(new Role(roleName), usersToAdd.getUserIds()); + eventEmitter.emitAssignRoleToUsers(roleName); + } + + @RequestMapping(value = "/api/role/{roleName}/remove/", method = RequestMethod.POST) + public void removeRoleToUsers(@PathVariable("roleName") String roleName, @RequestBody Users usersToRemove) { + permissionService.removeRoleToUsers(new Role(roleName), usersToRemove.getUserIds()); + eventEmitter.emitRemoveRoleToUsers(roleName); + } + + @RequestMapping(value = "/api/role/available-permissions", method = RequestMethod.GET) + public Map> existingPermissions() { + Map> byCategory = new LinkedHashMap<>(); + for (PermissionCategory pc : PermissionCategory.values()) { + byCategory.put(pc, new ArrayList()); + } + for (Permission permission : Permission.values()) { + byCategory.get(permission.getCategory()).add(permission); + } + return byCategory; + } +} diff --git a/src/main/java/io/lavagna/web/api/ProjectController.java b/src/main/java/io/lavagna/web/api/ProjectController.java new file mode 100644 index 000000000..bc386e4f6 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/ProjectController.java @@ -0,0 +1,211 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.BoardInfo; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.Permission; +import io.lavagna.model.Project; +import io.lavagna.model.UserWithPermission; +import io.lavagna.model.util.ShortNameGenerator; +import io.lavagna.service.BoardRepository; +import io.lavagna.service.StatisticsService; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.ProjectService; +import io.lavagna.service.SearchService; +import io.lavagna.web.api.model.CreateRequest; +import io.lavagna.web.api.model.Suggestion; +import io.lavagna.web.api.model.TaskStatistics; +import io.lavagna.web.api.model.TaskStatisticsAndHistory; +import io.lavagna.web.api.model.UpdateRequest; +import io.lavagna.web.api.model.ValidationException; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ProjectController { + + private final ProjectService projectService; + private final BoardRepository boardRepository; + private final EventEmitter eventEmitter; + private final StatisticsService statisticsService; + private final SearchService searchService; + + @Autowired + public ProjectController(ProjectService projectService, BoardRepository boardRepository, EventEmitter eventEmitter, + StatisticsService statisticsService, SearchService searchService) { + this.projectService = projectService; + this.boardRepository = boardRepository; + this.eventEmitter = eventEmitter; + this.statisticsService = statisticsService; + this.searchService = searchService; + } + + @RequestMapping(value = "/api/project", method = RequestMethod.GET) + public List findProjects(UserWithPermission user) { + if (user.getBasePermissions().containsKey(Permission.READ)) { + return projectService.findAll(); + } + return projectService.findAllForUserWithPermissionInProject(user); + } + + @ExpectPermission(Permission.CREATE_PROJECT) + @RequestMapping(value = "/api/project", method = RequestMethod.POST) + public void create(@RequestBody CreateRequest project) { + checkShortName(project.getShortName()); + projectService.create(project.getName(), project.getShortName(), project.getDescription()); + eventEmitter.emitCreateProject(project.getShortName()); + } + + @ExpectPermission(Permission.CREATE_PROJECT) + @RequestMapping(value = "/api/suggest-project-short-name", method = RequestMethod.GET) + public Suggestion suggestProjectShortName(@RequestParam("name") String name) { + return new Suggestion(ShortNameGenerator.generateShortNameFrom(name)); + } + + @ExpectPermission(Permission.CREATE_PROJECT) + @RequestMapping(value = "/api/check-project-short-name", method = RequestMethod.GET) + public boolean checkProjectShortName(@RequestParam("name") String name) { + return ShortNameGenerator.isShortNameValid(name) && !projectService.existsWithShortName(name); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}", method = RequestMethod.GET) + public Project findByShortName(@PathVariable("projectShortName") String shortName) { + return projectService.findByShortName(shortName); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/definitions", method = RequestMethod.GET) + public List findColumnDefinitions(@PathVariable("projectShortName") String shortName) { + Project project = projectService.findByShortName(shortName); + return projectService.findColumnDefinitionsByProjectId(project.getId()); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/board", method = RequestMethod.GET) + public List findBoards(@PathVariable("projectShortName") String shortName) { + Project project = projectService.findByShortName(shortName); + return boardRepository.findBoardInfo(project.getId()); + } + + @ExpectPermission(Permission.CREATE_BOARD) + @RequestMapping(value = "/api/project/{projectShortName}/board", method = RequestMethod.POST) + public void createBoard(@PathVariable("projectShortName") String shortName, @RequestBody CreateRequest board) { + checkShortName(board.getShortName()); + Project project = projectService.findByShortName(shortName); + boardRepository.createNewBoard(board.getName(), board.getShortName(), board.getDescription(), project.getId()); + eventEmitter.emitCreateBoard(project.getShortName()); + } + + @ExpectPermission(Permission.PROJECT_ADMINISTRATION) + @RequestMapping(value = "/api/project/{projectShortName}/definition", method = RequestMethod.PUT) + public int updateColumnDefinition(@PathVariable("projectShortName") String shortName, + @RequestBody UpdateColumnDefinition columnDefinition) { + Project project = projectService.findByShortName(shortName); + return projectService.updateColumnDefinition(project.getId(), columnDefinition.getDefinition(), + columnDefinition.getColor()); + } + + @ExpectPermission(Permission.PROJECT_ADMINISTRATION) + @RequestMapping(value = "/api/project/{projectShortName}", method = RequestMethod.POST) + public Project updateProject(@PathVariable("projectShortName") String shortName, + @RequestBody UpdateRequest updatedProject) { + Project project = projectService.findByShortName(shortName); + project = projectService.updateProject(project.getId(), updatedProject.getName(), + updatedProject.getDescription(), updatedProject.isArchived()); + eventEmitter.emitUpdateProject(shortName); + return project; + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/task-statistics", method = RequestMethod.GET) + public TaskStatistics projectTaskStatistics(@PathVariable("projectShortName") String projectShortName, + UserWithPermission user) { + int projectId = projectService.findByShortName(projectShortName).getId(); + + Map tasks = searchService + .findTaksByColumnDefinition(projectId, null, true, user); + + Map columnDefinitions = projectService + .findMappedColumnDefinitionsByProjectId(projectId); + + return new TaskStatistics(tasks, columnDefinitions); + } + + @ExpectPermission(Permission.READ) + @RequestMapping(value = "/api/project/{projectShortName}/statistics/{fromDate}", method = RequestMethod.GET) + public TaskStatisticsAndHistory projectStatistics(@PathVariable("projectShortName") String projectShortName, + @PathVariable("fromDate") Date fromDate, UserWithPermission user) { + int projectId = projectService.findByShortName(projectShortName).getId(); + + Map tasks = searchService + .findTaksByColumnDefinition(projectId, null, true, user); + + Map columnDefinitions = projectService + .findMappedColumnDefinitionsByProjectId(projectId); + + Map> cardStatus = statisticsService.getCardsStatusByProject(projectId, + fromDate); + + + Integer activeUsers = statisticsService.getActiveUsersOnProject(projectId, fromDate); + + return new TaskStatisticsAndHistory(tasks, columnDefinitions, cardStatus, + statisticsService.getCreatedAndClosedCardsByProject(projectId, fromDate), + activeUsers, + statisticsService.getAverageUsersPerCardOnProject(projectId), + statisticsService.getAverageCardsPerUserOnProject(projectId), + statisticsService.getCardsByLabelOnProject(projectId), + activeUsers > 0 ? statisticsService.getMostActiveCardByProject(projectId, fromDate) : null); + } + + /** + * Check the format of a short name. + *

+ * It must be composed only with uppercase alpha numeric ASCII characters and "_". + * + * @param shortName + */ + private static void checkShortName(String shortName) { + if (!ShortNameGenerator.isShortNameValid(shortName)) { + throw new ValidationException(); + } + } + + @Getter + @Setter + public static class UpdateColumnDefinition { + private int definition; + private int color; + } +} diff --git a/src/main/java/io/lavagna/web/api/ProjectPermissionController.java b/src/main/java/io/lavagna/web/api/ProjectPermissionController.java new file mode 100644 index 000000000..2844d9943 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/ProjectPermissionController.java @@ -0,0 +1,149 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Permission; +import io.lavagna.model.PermissionCategory; +import io.lavagna.model.Project; +import io.lavagna.model.Role; +import io.lavagna.model.RoleAndMetadata; +import io.lavagna.model.User; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.PermissionService; +import io.lavagna.service.PermissionService.RoleAndPermissions; +import io.lavagna.service.ProjectService; +import io.lavagna.web.api.model.CreateRole; +import io.lavagna.web.api.model.UpdateRole; +import io.lavagna.web.api.model.Users; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@ExpectPermission(Permission.PROJECT_ADMINISTRATION) +public class ProjectPermissionController { + + private final PermissionService permissionService; + private final EventEmitter eventEmitter; + private final ProjectService projectService; + + @Autowired + public ProjectPermissionController(PermissionService permissionService, EventEmitter eventEmitter, + ProjectService projectService) { + this.permissionService = permissionService; + this.eventEmitter = eventEmitter; + this.projectService = projectService; + } + + @RequestMapping(value = "/api/project/{projectShortName}/role", method = RequestMethod.GET) + public Map findAllRolesAndRelatedPermissions( + @PathVariable("projectShortName") String projectShortName) { + Project project = projectService.findByShortName(projectShortName); + return permissionService.findAllRolesAndRelatedPermissionInProjectId(project.getId()); + } + + @RequestMapping(value = "/api/project/{projectShortName}/role", method = RequestMethod.POST) + public int createRole(@PathVariable("projectShortName") String projectShortName, @RequestBody CreateRole newRole) { + Project project = projectService.findByShortName(projectShortName); + int res = permissionService.createRoleInProjectId(new Role(newRole.getName()), project.getId()); + eventEmitter.emitCreateRole(project.getShortName()); + return res; + } + + @RequestMapping(value = "/api/project/{projectShortName}/role/{roleName}", method = RequestMethod.POST) + public void updateRole(@PathVariable("projectShortName") String projectShortName, + @PathVariable("roleName") String roleName, @RequestBody UpdateRole updateRole) { + + Project project = projectService.findByShortName(projectShortName); + RoleAndMetadata role = permissionService.findRoleInProjectByName(project.getId(), + roleName); + + Validate.isTrue(!role.isReadOnly()); + + permissionService.updatePermissionsToRoleInProjectId(new Role(roleName), updateRole.getPermissions(), + project.getId()); + eventEmitter.emitUpdatePermissionsToRole(project.getShortName()); + } + + @RequestMapping(value = "/api/project/{projectShortName}/role/{roleName}", method = RequestMethod.DELETE) + public void deleteRole(@PathVariable("projectShortName") String projectShortName, + @PathVariable("roleName") String roleName) { + Project project = projectService.findByShortName(projectShortName); + + RoleAndMetadata role = permissionService.findRoleInProjectByName(project.getId(), + roleName); + + Validate.isTrue(role.isRemovable()); + + permissionService.deleteRoleInProjectId(new Role(roleName), project.getId()); + eventEmitter.emitDeleteRole(project.getShortName()); + } + + @RequestMapping(value = "/api/project/{projectShortName}/role/{roleName}/users/", method = RequestMethod.GET) + public List findUserByRole(@PathVariable("projectShortName") String projectShortName, + @PathVariable("roleName") String roleName) { + Project project = projectService.findByShortName(projectShortName); + return permissionService.findUserByRoleAndProjectId(new Role(roleName), project.getId()); + } + + @RequestMapping(value = "/api/project/{projectShortName}/role/{roleName}/users/", method = RequestMethod.POST) + public void assignUsersToRole(@PathVariable("projectShortName") String projectShortName, + @PathVariable("roleName") String roleName, @RequestBody Users usersToAdd) { + + Project project = projectService.findByShortName(projectShortName); + permissionService.assignRoleToUsersInProjectId(new Role(roleName), usersToAdd.getUserIds(), project.getId()); + eventEmitter.emitAssignRoleToUsers(roleName, project.getShortName()); + } + + @RequestMapping(value = "/api/project/{projectShortName}/role/{roleName}/remove/", method = RequestMethod.POST) + public void removeRoleToUsers(@PathVariable("projectShortName") String projectShortName, + @PathVariable("roleName") String roleName, @RequestBody Users usersToRemove) { + + Project project = projectService.findByShortName(projectShortName); + permissionService.removeRoleToUsersInProjectId(new Role(roleName), usersToRemove.getUserIds(), project.getId()); + eventEmitter.emitRemoveRoleToUsers(roleName, project.getShortName()); + } + + @RequestMapping(value = "/api/project/{projectShortName}/role/available-permissions", method = RequestMethod.GET) + public Map> existingPermissions( + @PathVariable("projectShortName") String projectShortName) { + Map> byCategory = new LinkedHashMap<>(); + for (PermissionCategory pc : PermissionCategory.values()) { + if (!pc.isOnlyForBase()) { + byCategory.put(pc, new ArrayList()); + } + } + for (Permission permission : Permission.values()) { + if (!permission.isOnlyForBase() && byCategory.containsKey(permission.getCategory())) { + byCategory.get(permission.getCategory()).add(permission); + } + } + return byCategory; + } + +} diff --git a/src/main/java/io/lavagna/web/api/SearchController.java b/src/main/java/io/lavagna/web/api/SearchController.java new file mode 100644 index 000000000..84ed458d8 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/SearchController.java @@ -0,0 +1,171 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.common.Json; +import io.lavagna.model.CardFull; +import io.lavagna.model.CardLabel.LabelDomain; +import io.lavagna.model.Permission; +import io.lavagna.model.SearchResults; +import io.lavagna.model.User; +import io.lavagna.model.UserWithPermission; +import io.lavagna.service.CardLabelRepository; +import io.lavagna.service.CardRepository; +import io.lavagna.service.ProjectService; +import io.lavagna.service.SearchFilter; +import io.lavagna.service.SearchService; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.ExpectPermission; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.gson.reflect.TypeToken; + +@ExpectPermission(Permission.SEARCH) +@RestController +public class SearchController { + + private final UserRepository userRepository; + private final CardRepository cardRepository; + private final SearchService searchService; + private final CardLabelRepository cardLabelRepository; + private final ProjectService projectService; + + private static final Type LIST_OF_SEARCH_FILTERS = new TypeToken>() { + }.getType(); + + @Autowired + public SearchController(UserRepository userRepository, CardRepository cardRepository, + CardLabelRepository cardLabelRepository, SearchService searchService, ProjectService projectService) { + this.userRepository = userRepository; + this.cardRepository = cardRepository; + this.cardLabelRepository = cardLabelRepository; + this.searchService = searchService; + this.projectService = projectService; + } + + /** + * Given a list of user identifier "provider:username" it return a map name -> id. + * + * @return + */ + @RequestMapping(value = "/api/search/user-mapping", method = RequestMethod.GET) + public Map findUsersId(@RequestParam("from") List users) { + return userRepository.findUsersId(users); + } + + /** + * Given a list of card identifier "PROJECT-CARD_SEQUENCE" it return a map card identifier -> id Used by search.js + * + * @return + */ + @RequestMapping(value = "/api/search/card-mapping", method = RequestMethod.GET) + public Map findCardsIds(@RequestParam("from") List cards) { + return cardRepository.findCardsIds(cards); + } + + /** + * Given a list of label list value, return a map value -> card label id -> card label list value id + * + * @param labelValues + * @return + */ + @RequestMapping(value = "/api/search/label-list-value-mapping", method = RequestMethod.GET) + public Map> findLabelListValueMapping(@RequestParam("from") List labelValues) { + return cardLabelRepository.findLabelListValueMapping(labelValues); + } + + @RequestMapping(value = "/api/search/card", method = RequestMethod.GET) + public SearchResults search(@RequestParam("q") String queryAsJson, + @RequestParam(value = "projectName", required = false) String projectName, + @RequestParam(value = "page", required = false, defaultValue = "0") int page, + UserWithPermission userWithPermission) { + List searchFilters = Json.GSON.fromJson(queryAsJson, LIST_OF_SEARCH_FILTERS); + Integer projectId = toProjectId(projectName); + return searchService.find(searchFilters, projectId, null, userWithPermission, page); + } + + @RequestMapping(value = "/api/search/user", method = RequestMethod.GET) + public List findUsers(@RequestParam("term") String term, + @RequestParam(value = "projectName", required = false) String projectName, + UserWithPermission userWithPermission) { + + // TODO: ugly code + boolean useProjectSearch = StringUtils.isNotBlank(projectName); + if (useProjectSearch && !hasReadAccessToProject(userWithPermission, projectName)) { + return Collections.emptyList(); + } + + String[] splitted = StringUtils.split(term, ':'); + + if (term != null && splitted.length > 0) { + String value = splitted.length == 1 ? splitted[0] : splitted[1]; + return useProjectSearch ? userRepository.findUsers(value, projectService.findByShortName(projectName).getId(), Permission.READ) : userRepository.findUsers(value); + } else { + return Collections.emptyList(); + } + } + + private static boolean hasReadAccessToProject(UserWithPermission userWithPermission, String projectName) { + return (userWithPermission.getBasePermissions().containsKey(Permission.READ) || userWithPermission + .projectsWithPermission(Permission.READ).contains(projectName)); + } + + @RequestMapping(value = "/api/search/autocomplete-card", method = RequestMethod.GET) + public List searchCardForAutocomplete(@RequestParam("term") String term, + UserWithPermission userWithPermission) { + + boolean hasGlobalAccess = userWithPermission.getBasePermissions().containsKey(Permission.READ); + Set projectReadAccess = userWithPermission.projectsIdWithPermission(Permission.READ); + Validate.isTrue(hasGlobalAccess || !projectReadAccess.isEmpty()); + + return cardRepository.findCardBy(term, hasGlobalAccess ? null : projectReadAccess); + } + + @RequestMapping(value = "/api/search/milestone", method = RequestMethod.GET) + public List findMilestones(@RequestParam("term") String term, + @RequestParam(value = "projectName", required = false) String projectName, + UserWithPermission userWithPermission) { + Integer projectId = toProjectId(projectName); + return cardLabelRepository.findListValuesBy(LabelDomain.SYSTEM, "MILESTONE", term, projectId, + userWithPermission); + } + + @RequestMapping(value = "/api/search/label-name", method = RequestMethod.GET) + public List findLabelNames(@RequestParam("term") String term, + @RequestParam(value = "projectName", required = false) String projectName, + UserWithPermission userWithPermission) { + Integer projectId = toProjectId(projectName); + return cardLabelRepository.findUserLabelNameBy(term, projectId, userWithPermission); + } + + private Integer toProjectId(String projectName) { + return projectName == null ? null : projectService.findByShortName(projectName).getId(); + } +} diff --git a/src/main/java/io/lavagna/web/api/SetupController.java b/src/main/java/io/lavagna/web/api/SetupController.java new file mode 100644 index 000000000..c53cf35d9 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/SetupController.java @@ -0,0 +1,102 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Pair; +import io.lavagna.model.UserToCreate; +import io.lavagna.service.ExportImportService; +import io.lavagna.service.Ldap; +import io.lavagna.service.SetupService; +import io.lavagna.web.api.model.Conf; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.Getter; +import lombok.Setter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; + +@Controller +public class SetupController { + + private final Ldap ldap; + private final ExportImportService exportImportService2; + private final SetupService setupService; + + @Autowired + public SetupController(SetupService setupService, Ldap ldap, ExportImportService exportImportService2) { + + this.ldap = ldap; + this.exportImportService2 = exportImportService2; + this.setupService = setupService; + } + + @RequestMapping(value = "/setup/api/setup/", method = RequestMethod.POST) + @ResponseBody + public void setup(@RequestBody ConfWithUser conf) { + setupService.updateConfiguration(conf.getToUpdateOrCreate(), conf.getUser()); + } + + @RequestMapping(value = "/setup/api/check-ldap/", method = RequestMethod.POST) + @ResponseBody + public Pair> checkLdap(@RequestBody Map r) { + return ldap.authenticateWithParams(r.get("serverUrl"), r.get("managerDn"), r.get("managerPassword"), + r.get("userSearchBase"), r.get("userSearchFilter"), r.get("username"), r.get("password")); + } + + @RequestMapping(value = "/setup/api/import", method = RequestMethod.POST) + public void importLavagna(@RequestParam("file") MultipartFile file, HttpServletRequest req, HttpServletResponse res) + throws IOException { + + // TODO: move to a helper, as it has the same code as the one in the ExportImportController + Path tempFile = Files.createTempFile(null, null); + try { + try (InputStream is = file.getInputStream(); OutputStream os = Files.newOutputStream(tempFile)) { + StreamUtils.copy(is, os); + } + exportImportService2.importData(true, tempFile); + } finally { + Files.delete(tempFile); + } + // + + res.sendRedirect(req.getServletContext().getContextPath()); + } + + @Getter + @Setter + public static class ConfWithUser extends Conf { + private UserToCreate user; + } +} diff --git a/src/main/java/io/lavagna/web/api/UserController.java b/src/main/java/io/lavagna/web/api/UserController.java new file mode 100644 index 000000000..9618e79fd --- /dev/null +++ b/src/main/java/io/lavagna/web/api/UserController.java @@ -0,0 +1,162 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.model.Event; +import io.lavagna.model.EventsCount; +import io.lavagna.model.Permission; +import io.lavagna.model.ProjectWithEventCounts; +import io.lavagna.model.User; +import io.lavagna.model.UserWithPermission; +import io.lavagna.service.EventEmitter; +import io.lavagna.service.EventRepository; +import io.lavagna.service.ProjectService; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.ExpectPermission; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UserController { + + private final UserRepository userRepository; + private final EventEmitter eventEmitter; + private final EventRepository eventRepository; + private final ProjectService projectService; + + @Autowired + public UserController(UserRepository userRepository, EventEmitter eventEmitter, EventRepository eventRepository, + ProjectService projectService) { + this.userRepository = userRepository; + this.eventEmitter = eventEmitter; + this.eventRepository = eventRepository; + this.projectService = projectService; + } + + @RequestMapping(value = "/api/self", method = RequestMethod.GET) + // user is resolved through UserArgumentResolver + public UserWithPermission userProfile(UserWithPermission user) { + return user; + } + + @RequestMapping(value = "/api/self/clear-all-tokens", method = RequestMethod.POST) + public void clearAllTokens(UserWithPermission currentUser) { + userRepository.clearAllTokens(currentUser); + } + + @RequestMapping(value = "/api/self/feed/{page}", method = RequestMethod.GET) + // user is resolved through UserArgumentResolver + public List userFeed(@PathVariable("page") int page, UserWithPermission user) { + return eventRepository.getUserFeedByPage(user.getId(), page); + } + + @ExpectPermission(Permission.UPDATE_PROFILE) + @RequestMapping(value = "/api/self", method = RequestMethod.POST) + public int updateUserProfile(UserWithPermission user, @RequestBody DisplayNameEmail toUpdate) { + int result = userRepository.updateProfile(user, toUpdate.getEmail(), toUpdate.getDisplayName(), + toUpdate.isEmailNotification()); + eventEmitter.emitUpdateUserProfile(user.getId()); + return result; + } + + @RequestMapping(value = "/api/user/{userId}", method = RequestMethod.GET) + public User getUser(@PathVariable("userId") int userId) { + return userRepository.findById(userId); + } + + @RequestMapping(value = "/api/user/profile/{provider}/{name}", method = RequestMethod.GET) + public UserPublicProfile getUserProfile(@PathVariable("provider") String provider, + @PathVariable("name") String name, UserWithPermission currentUser, + @RequestParam(value = "page", defaultValue = "0") int page) { + User user = userRepository.findUserByName(provider, name); + + final List dailyActivity; + final List activeProjects; + final List latestActivity; + Date fromDate = DateUtils.setDays(DateUtils.addMonths(new Date(), -11), 1); + if (currentUser.getBasePermissions().containsKey(Permission.READ)) { + dailyActivity = eventRepository.getUserActivity(user.getId(), fromDate); + activeProjects = projectService.findProjectsActivityByUser(user.getId()); + latestActivity = eventRepository.getLatestActivityByPage(user.getId(), page); + } else { + Collection visibleProjectsIds = currentUser.projectsIdWithPermission(Permission.READ); + + dailyActivity = eventRepository.getUserActivityForProjects(user.getId(), fromDate, visibleProjectsIds); + activeProjects = projectService.findProjectsActivityByUserInProjects(user.getId(), + visibleProjectsIds); + latestActivity = eventRepository.getLatestActivityByPageAndProjects(user.getId(), page, visibleProjectsIds); + } + + return new UserPublicProfile(user, dailyActivity, activeProjects, latestActivity); + } + + @RequestMapping(value = "/api/user/{provider}/{name}", method = RequestMethod.GET) + public User getUser(@PathVariable("provider") String provider, @PathVariable("name") String name) { + return userRepository.findUserByName(provider, name); + } + + @RequestMapping(value = "/api/keep-alive", method = RequestMethod.GET) + public boolean keepAlive() { + return true; + } + + @ExpectPermission(Permission.ADMINISTRATION) + @RequestMapping(value = "/api/user/list", method = RequestMethod.GET) + public List findAllUsers() { + return userRepository.findAll(); + } + + @Getter + @Setter + public static class DisplayNameEmail { + private String email; + private String displayName; + private boolean emailNotification; + } + + @Getter + public static class UserPublicProfile { + private final User user; + private final List dailyActivity; + private final List activeProjects; + private final List latestActivity; + + public UserPublicProfile(User user, List dailyActivity, + List activeProjects, List latestActivity) { + // we remove the email + this.user = new User(user.getId(), user.getProvider(), user.getUsername(), null, user.getDisplayName(), + user.isEnabled(), user.isEmailNotification(), user.getMemberSince()); + this.activeProjects = activeProjects; + this.dailyActivity = dailyActivity; + this.latestActivity = latestActivity; + } + } +} diff --git a/src/main/java/io/lavagna/web/api/UsersAdministrationController.java b/src/main/java/io/lavagna/web/api/UsersAdministrationController.java new file mode 100644 index 000000000..b9654218c --- /dev/null +++ b/src/main/java/io/lavagna/web/api/UsersAdministrationController.java @@ -0,0 +1,90 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import io.lavagna.common.Json; +import io.lavagna.model.Permission; +import io.lavagna.model.User; +import io.lavagna.model.UserToCreate; +import io.lavagna.service.UserRepository; +import io.lavagna.service.UserService; +import io.lavagna.web.helper.ExpectPermission; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.google.gson.reflect.TypeToken; + +@RestController +@ExpectPermission(Permission.ADMINISTRATION) +public class UsersAdministrationController { + + private final Type userToCreateListType = new TypeToken>() { + }.getType(); + + private final UserRepository userRepository; + private final UserService userService; + + @Autowired + public UsersAdministrationController(UserRepository userRepository, UserService userService) { + this.userRepository = userRepository; + this.userService = userService; + } + + @RequestMapping(value = "/api/user/{userId}/enable", method = RequestMethod.POST) + public void toggle(@PathVariable("userId") int userId, User user, @RequestBody Update status) { + Validate.isTrue(user.getId() != userId, "cannot update the status"); + userRepository.toggle(userId, status.enabled); + } + + @RequestMapping(value = "/api/user/insert", method = RequestMethod.POST) + public void createUser(@RequestBody UserToCreate userToCreate) { + userService.createUser(userToCreate); + } + + @RequestMapping(value = "/api/user/bulk-insert", method = RequestMethod.POST) + public void createUsers(@RequestParam("file") MultipartFile file) throws IOException { + try (InputStream is = file.getInputStream(); + InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) { + List usersToCreate = Json.GSON.fromJson(isr, userToCreateListType); + userService.createUsers(usersToCreate); + } + } + + @Getter + @Setter + public static class Update { + private boolean enabled; + } +} diff --git a/src/main/java/io/lavagna/web/api/Utils.java b/src/main/java/io/lavagna/web/api/Utils.java new file mode 100644 index 000000000..c5bedaa23 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/Utils.java @@ -0,0 +1,37 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api; + +import java.util.ArrayList; +import java.util.List; + +abstract class Utils { + + /** + * CHECK: After updating to spring4rc2 we receive List of Double o_O + * + * @param v + * @return + */ + static List from(List v) { + List res = new ArrayList<>(); + for (Number n : v) { + res.add(n.intValue()); + } + return res; + } +} diff --git a/src/main/java/io/lavagna/web/api/model/Conf.java b/src/main/java/io/lavagna/web/api/model/Conf.java new file mode 100644 index 000000000..3c7927939 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/Conf.java @@ -0,0 +1,30 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.ConfigurationKeyValue; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Conf { + private List toUpdateOrCreate; +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/api/model/CreateRequest.java b/src/main/java/io/lavagna/web/api/model/CreateRequest.java new file mode 100644 index 000000000..97f9e67fd --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/CreateRequest.java @@ -0,0 +1,26 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CreateRequest extends UpdateRequest { + private String shortName; +} diff --git a/src/main/java/io/lavagna/web/api/model/CreateRole.java b/src/main/java/io/lavagna/web/api/model/CreateRole.java new file mode 100644 index 000000000..0548ddfa2 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/CreateRole.java @@ -0,0 +1,26 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CreateRole { + private String name; +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/api/model/MilestoneDetail.java b/src/main/java/io/lavagna/web/api/model/MilestoneDetail.java new file mode 100644 index 000000000..013c8aa4b --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/MilestoneDetail.java @@ -0,0 +1,32 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.Pair; +import io.lavagna.model.SearchResults; + +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MilestoneDetail { + private final SearchResults cards; + private final Map> assignedAndClosedCards; +} diff --git a/src/main/java/io/lavagna/web/api/model/MilestoneInfo.java b/src/main/java/io/lavagna/web/api/model/MilestoneInfo.java new file mode 100644 index 000000000..4b45fd4c8 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/MilestoneInfo.java @@ -0,0 +1,32 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.LabelListValue; + +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MilestoneInfo { + private final LabelListValue labelListValue; + private final Map cardsCountByStatus; +} diff --git a/src/main/java/io/lavagna/web/api/model/Milestones.java b/src/main/java/io/lavagna/web/api/model/Milestones.java new file mode 100644 index 000000000..ec96db082 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/Milestones.java @@ -0,0 +1,33 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.ColumnDefinition; + +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Milestones { + + private final List milestones; + private final Map statusColors; +} diff --git a/src/main/java/io/lavagna/web/api/model/Suggestion.java b/src/main/java/io/lavagna/web/api/model/Suggestion.java new file mode 100644 index 000000000..5d06ff304 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/Suggestion.java @@ -0,0 +1,26 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Suggestion { + private final String suggestion; +} diff --git a/src/main/java/io/lavagna/web/api/model/TaskStatistics.java b/src/main/java/io/lavagna/web/api/model/TaskStatistics.java new file mode 100644 index 000000000..39789ee86 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/TaskStatistics.java @@ -0,0 +1,52 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.ColumnDefinition; + +import java.util.Map; + +import lombok.Getter; + +@Getter +public class TaskStatistics { + + private final int openTaskColor; + private final int closedTaskColor; + private final int backlogTaskColor; + private final int deferredTaskColor; + + private final int openTaskCount; + private final int closedTaskCount; + private final int backlogTaskCount; + private final int deferredTaskCount; + + public TaskStatistics(Map tasks, + Map columnDefinitions) { + + this.openTaskColor = columnDefinitions.get(ColumnDefinition.OPEN).getColor(); + this.closedTaskColor = columnDefinitions.get(ColumnDefinition.CLOSED).getColor(); + this.backlogTaskColor = columnDefinitions.get(ColumnDefinition.BACKLOG).getColor(); + this.deferredTaskColor = columnDefinitions.get(ColumnDefinition.DEFERRED).getColor(); + + this.openTaskCount = tasks.get(ColumnDefinition.OPEN); + this.closedTaskCount = tasks.get(ColumnDefinition.CLOSED); + this.backlogTaskCount = tasks.get(ColumnDefinition.BACKLOG); + this.deferredTaskCount = tasks.get(ColumnDefinition.DEFERRED); + } +} diff --git a/src/main/java/io/lavagna/web/api/model/TaskStatisticsAndHistory.java b/src/main/java/io/lavagna/web/api/model/TaskStatisticsAndHistory.java new file mode 100644 index 000000000..ed328ac36 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/TaskStatisticsAndHistory.java @@ -0,0 +1,55 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.BoardColumnDefinition; +import io.lavagna.model.CardFull; +import io.lavagna.model.ColumnDefinition; +import io.lavagna.model.LabelAndValueWithCount; +import io.lavagna.model.Pair; + +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +@Getter +public class TaskStatisticsAndHistory extends TaskStatistics { + + private final Map> statusHistory; + private final Map> createdAndClosedCards; + private final List cardsByLabel; + private final Integer activeUsers; + private final double averageUsersPerCard; + private final double averageCardsPerUser; + private final CardFull mostActiveCard; + + public TaskStatisticsAndHistory(Map tasks, + Map columnDefinitions, + Map> statusHistory, Map> createdAndClosedCards, + Integer activeUsers, double averageUsersPerCard, double averageCardsPerUser, + List cardsByLabel, CardFull mostActiveCard) { + super(tasks, columnDefinitions); + this.statusHistory = statusHistory; + this.createdAndClosedCards = createdAndClosedCards; + this.activeUsers = activeUsers; + this.averageUsersPerCard = averageUsersPerCard; + this.averageCardsPerUser = averageCardsPerUser; + this.cardsByLabel = cardsByLabel; + this.mostActiveCard = mostActiveCard; + } +} diff --git a/src/main/java/io/lavagna/web/api/model/TrelloImportRequest.java b/src/main/java/io/lavagna/web/api/model/TrelloImportRequest.java new file mode 100644 index 000000000..4ecd59d9d --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/TrelloImportRequest.java @@ -0,0 +1,37 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TrelloImportRequest extends TrelloRequest { + private String projectShortName; + private List boards; + private String importId; + + @Getter + @Setter + public static class BoardIdAndShortName { + private String id; + private String shortName; + } +} diff --git a/src/main/java/io/lavagna/web/api/model/TrelloRequest.java b/src/main/java/io/lavagna/web/api/model/TrelloRequest.java new file mode 100644 index 000000000..b546d220b --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/TrelloRequest.java @@ -0,0 +1,27 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TrelloRequest { + private String apiKey; + private String secret; +} diff --git a/src/main/java/io/lavagna/web/api/model/UpdateRequest.java b/src/main/java/io/lavagna/web/api/model/UpdateRequest.java new file mode 100644 index 000000000..15b6f305c --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/UpdateRequest.java @@ -0,0 +1,28 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UpdateRequest { + private String name; + private String description; + private boolean archived; +} diff --git a/src/main/java/io/lavagna/web/api/model/UpdateRole.java b/src/main/java/io/lavagna/web/api/model/UpdateRole.java new file mode 100644 index 000000000..8dcc1bb53 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/UpdateRole.java @@ -0,0 +1,30 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import io.lavagna.model.Permission; + +import java.util.Set; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UpdateRole { + private Set permissions; +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/api/model/Users.java b/src/main/java/io/lavagna/web/api/model/Users.java new file mode 100644 index 000000000..d8310ad3a --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/Users.java @@ -0,0 +1,28 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +import java.util.Set; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Users { + private Set userIds; +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/api/model/ValidationException.java b/src/main/java/io/lavagna/web/api/model/ValidationException.java new file mode 100644 index 000000000..3d42fded5 --- /dev/null +++ b/src/main/java/io/lavagna/web/api/model/ValidationException.java @@ -0,0 +1,23 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.api.model; + +public class ValidationException extends IllegalArgumentException { + + private static final long serialVersionUID = 7900005694710945424L; + +} diff --git a/src/main/java/io/lavagna/web/helper/AbstractPermissionUrlPath.java b/src/main/java/io/lavagna/web/helper/AbstractPermissionUrlPath.java new file mode 100644 index 000000000..2a6c7bceb --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/AbstractPermissionUrlPath.java @@ -0,0 +1,229 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.service.ProjectService; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class AbstractPermissionUrlPath { + + private static final Logger LOG = LogManager.getLogger(); + + private final String path; + private final Pattern pattern; + + AbstractPermissionUrlPath(String path, String paramName) { + this.path = path; + + // we have an internal regex + if (paramName.contains(":")) { + this.pattern = Pattern.compile(path.replace("{" + paramName + "}", "(" + (paramName.split(":")[1]) + ")")); + } else { + this.pattern = Pattern.compile(path.replace("{" + paramName + "}", "([^/]+)")); + } + } + + Set tryToFetchProjectShortNames(String requestUri, ProjectService projectService) { + LOG.trace("tryToFetchProjectShortNames : uri : {}, pattern: {}", requestUri, pattern); + Set e = extractAll(requestUri); + if (e.isEmpty()) { + return Collections.emptySet(); + } + return tryToFetchProjectShortName(e, projectService); + } + + /** + * Most of the cases the url has a single id, should not be a performance issue + * + * @param ids + * @param projectService + * @return + */ + protected abstract Set tryToFetchProjectShortName(Set ids, ProjectService projectService); + + private Set extractAll(String uri) { + Matcher m = pattern.matcher(uri); + Set groups = new HashSet<>(); + while (m.find()) { + groups.add(m.group(1)); + } + + LOG.trace("extract all : uri : {}, pattern: {}, groups: {}", uri, pattern, groups); + return groups; + } + + private static void addIfNotNull(Set s, String i) { + if (i != null) { + s.add(i); + } + } + + private static Set from(Set ids) { + Set res = new HashSet<>(); + for (String id : ids) { + try { + res.add(Integer.parseInt(id)); + } catch (NumberFormatException e) { + // nope + } + } + return res; + } + + static class ProjectShortNameUrlPath extends AbstractPermissionUrlPath { + ProjectShortNameUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set shortName, ProjectService projectService) { + return shortName; + } + } + + static class BoardShortNameUrlPath extends AbstractPermissionUrlPath { + BoardShortNameUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (String shortName : ids) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByBoardShortname(shortName)); + } + return res; + } + } + + static class CardIdUrlPath extends AbstractPermissionUrlPath { + CardIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (int cardId : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByCardId(cardId)); + } + return res; + } + } + + static class EventIdUrlPath extends AbstractPermissionUrlPath { + public EventIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + protected Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (int eventId : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByEventId(eventId)); + } + return res; + } + } + + static class CardDataIdUrlPath extends AbstractPermissionUrlPath { + CardDataIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (int cardDataId : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByCardDataId(cardDataId)); + } + return res; + } + } + + static class ColumnIdUrlPath extends AbstractPermissionUrlPath { + ColumnIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (Integer columnId : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByColumnId(columnId)); + } + return res; + } + } + + static class LabelIdUrlPath extends AbstractPermissionUrlPath { + LabelIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (Integer columnId : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByLabelId(columnId)); + } + return res; + } + } + + static class LabelValueIdUrlPath extends AbstractPermissionUrlPath { + LabelValueIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (Integer columnId : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByLabelValueId(columnId)); + } + return res; + } + } + + static class ColumnDefinitionIdUrlPath extends AbstractPermissionUrlPath { + ColumnDefinitionIdUrlPath(String path, String paramName) { + super(path, paramName); + } + + @Override + public Set tryToFetchProjectShortName(Set ids, ProjectService projectService) { + Set res = new HashSet<>(); + for (Integer id : from(ids)) { + addIfNotNull(res, projectService.findRelatedProjectShortNameByColumnDefinitionId(id)); + } + return res; + } + } + + public String getPath() { + return path; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/helper/CSRFToken.java b/src/main/java/io/lavagna/web/helper/CSRFToken.java new file mode 100644 index 000000000..5241c0269 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/CSRFToken.java @@ -0,0 +1,71 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import java.util.regex.Pattern; + +public final class CSRFToken { + + private CSRFToken() { + } + + // + public static final String CSRF_TOKEN = CSRFToken.class.getName() + ".CSRF_TOKEN"; + public static final String CSRF_TOKEN_HEADER = "X-CSRF-TOKEN"; + public static final String CSRF_FORM_PARAMETER = "_csrf"; + public static final Pattern CSRF_METHOD_DONT_CHECK = Pattern.compile("^GET|HEAD|OPTIONS$"); + + // ------------------------------------------------------------------------ + // this function has been imported from KeyCzar. + + /* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for + * the specific language governing permissions and limitations under the License. + */ + + /** + * An array comparison that is safe from timing attacks. If two arrays are of equal length, this code will always + * check all elements, rather than exiting once it encounters a differing byte. + * + * @param a1 + * An array to compare + * @param a2 + * Another array to compare + * @return True if these arrays are both null or if they have equal length and equal bytes in all elements + */ + public static boolean safeArrayEquals(byte[] a1, byte[] a2) { + if (a1 == null || a2 == null) { + return a1 == a2; + } + if (a1.length != a2.length) { + return false; + } + byte result = 0; + for (int i = 0; i < a1.length; i++) { + result |= a1[i] ^ a2[i]; + } + return result == 0; + } +} diff --git a/src/main/java/io/lavagna/web/helper/CardCommentOwnershipChecker.java b/src/main/java/io/lavagna/web/helper/CardCommentOwnershipChecker.java new file mode 100644 index 000000000..6ca39b099 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/CardCommentOwnershipChecker.java @@ -0,0 +1,59 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.UserWithPermission; +import io.lavagna.model.Event.EventType; +import io.lavagna.service.EventRepository; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class CardCommentOwnershipChecker implements OwnershipChecker { + + private final Pattern pattern = Pattern.compile("^.*/comment/(\\d+)$"); + + private final EventRepository eventRepository; + + @Autowired + public CardCommentOwnershipChecker(EventRepository eventRepository) { + this.eventRepository = eventRepository; + } + + @Override + public boolean hasOwnership(HttpServletRequest request, UserWithPermission user) { + + Matcher matcher = pattern.matcher(request.getRequestURI()); + try { + if (matcher.matches()) { + int userId = UserSession.getUserId(request); + int commentId = Integer.parseInt(matcher.group(1), 10); + return eventRepository.findUsersIdFor(commentId, EventType.COMMENT_CREATE).contains(userId); + } + } catch (NumberFormatException nfe) { + return false; + } + + return false; + } +} diff --git a/src/main/java/io/lavagna/web/helper/ExpectPermission.java b/src/main/java/io/lavagna/web/helper/ExpectPermission.java new file mode 100644 index 000000000..3244b074b --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/ExpectPermission.java @@ -0,0 +1,65 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.Permission; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.web.method.HandlerMethod; + +/** + * The annotation can be used at both method and class level. Method level annotation will take precedence over the + * class one.
+ * + * If an user does not have the related permission, a 403 error will be triggered. + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface ExpectPermission { + Permission value(); + + Class ownershipChecker() default NoOpOwnershipChecker.class; + + public static final class Helper { + + private Helper() { + } + + public static ExpectPermission getAnnotation(HandlerMethod handler) { + return getAnnotation((Object) handler); + } + + static ExpectPermission getAnnotation(Object handler) { + if (handler == null || !(handler instanceof HandlerMethod)) { + return null; + } + + HandlerMethod hm = (HandlerMethod) handler; + + ExpectPermission expectPermission = hm.getMethodAnnotation(ExpectPermission.class); + if (expectPermission == null) { + expectPermission = hm.getBeanType().getAnnotation(ExpectPermission.class); + } + return expectPermission; + } + } +} diff --git a/src/main/java/io/lavagna/web/helper/GeneralHandlerExceptionResolver.java b/src/main/java/io/lavagna/web/helper/GeneralHandlerExceptionResolver.java new file mode 100644 index 000000000..dbadff042 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/GeneralHandlerExceptionResolver.java @@ -0,0 +1,70 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.web.api.model.ValidationException; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +public class GeneralHandlerExceptionResolver implements HandlerExceptionResolver { + + private static final Logger LOG = LogManager.getLogger(); + + private final Map, Integer> statusCodeResolver = new LinkedHashMap<>(); + + public GeneralHandlerExceptionResolver() { + // add the exceptions from the less generic to the more one + statusCodeResolver.put(EmptyResultDataAccessException.class, HttpStatus.NOT_FOUND.value()); + statusCodeResolver.put(ValidationException.class, HttpStatus.UNPROCESSABLE_ENTITY.value()); + } + + @Override + public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, + Exception ex) { + handleException(ex, response); + return new ModelAndView(); + } + + private void handleException(Exception ex, HttpServletResponse response) { + for (Entry, Integer> entry : statusCodeResolver.entrySet()) { + if (ex.getClass().equals(entry.getKey())) { + response.setStatus(entry.getValue()); + LOG.info("Class: {} - Message: {} - Cause: {}", ex.getClass(), ex.getMessage(), ex.getCause()); + LOG.info("Cnt", ex); + return; + } + } + /** + * Non managed exceptions flow Set HTTP status 500 and log the exception with a production visible level + */ + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + LOG.warn(ex.getMessage(), ex); + } + +} diff --git a/src/main/java/io/lavagna/web/helper/GsonHttpMessageConverter.java b/src/main/java/io/lavagna/web/helper/GsonHttpMessageConverter.java new file mode 100644 index 000000000..908720f1d --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/GsonHttpMessageConverter.java @@ -0,0 +1,75 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +/** + * + * Read/write json, derived from : + *

    + *
  • http://stackoverflow.com/a/5099622 + *
  • http://stackoverflow.com/a/8728500 + *
+ * + * Used as a lightweight alternative to jackson. + */ +public class GsonHttpMessageConverter extends AbstractHttpMessageConverter { + + private final Gson gson = new GsonBuilder().serializeNulls().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .generateNonExecutableJson().create(); + + public GsonHttpMessageConverter() { + super(new MediaType("application", "json", StandardCharsets.UTF_8)); + } + + @Override + protected boolean supports(Class clazz) { + return true; + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + try (Reader reader = new InputStreamReader(inputMessage.getBody(), StandardCharsets.UTF_8)) { + return gson.fromJson(reader, clazz); + } catch (JsonSyntaxException e) { + throw new HttpMessageNotReadableException("Could not read JSON: " + e.getMessage(), e); + } + } + + @Override + protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException { + try (Writer writer = new OutputStreamWriter(outputMessage.getBody(), StandardCharsets.UTF_8)) { + gson.toJson(t, writer); + } + } +} diff --git a/src/main/java/io/lavagna/web/helper/NoOpOwnershipChecker.java b/src/main/java/io/lavagna/web/helper/NoOpOwnershipChecker.java new file mode 100644 index 000000000..a8754ffb7 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/NoOpOwnershipChecker.java @@ -0,0 +1,32 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.UserWithPermission; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.stereotype.Component; + +@Component +public class NoOpOwnershipChecker implements OwnershipChecker { + + @Override + public boolean hasOwnership(HttpServletRequest request, UserWithPermission user) { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/helper/OwnershipChecker.java b/src/main/java/io/lavagna/web/helper/OwnershipChecker.java new file mode 100644 index 000000000..6a828207b --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/OwnershipChecker.java @@ -0,0 +1,26 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.UserWithPermission; + +import javax.servlet.http.HttpServletRequest; + +public interface OwnershipChecker { + + boolean hasOwnership(HttpServletRequest request, UserWithPermission user); +} diff --git a/src/main/java/io/lavagna/web/helper/PermissionMethodInterceptor.java b/src/main/java/io/lavagna/web/helper/PermissionMethodInterceptor.java new file mode 100644 index 000000000..a4669db48 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/PermissionMethodInterceptor.java @@ -0,0 +1,168 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.Permission; +import io.lavagna.model.UserWithPermission; +import io.lavagna.service.ProjectService; +import io.lavagna.service.UserService; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +/** + * Interceptor for enforcing {@link ExpectPermission} annotation. See + * {@link #preHandle(HttpServletRequest, HttpServletResponse, Object)} code. + */ +@Component +public class PermissionMethodInterceptor extends HandlerInterceptorAdapter { + + @Autowired + private UserService userService; + + @Autowired + private ProjectService projectService; + + @Autowired + private WebApplicationContext context; + + public static final Set URL_PATTERNS_TO_CHECK; + + static { + Set p = new HashSet<>(); + + p.add(new AbstractPermissionUrlPath.BoardShortNameUrlPath("/board/{shortName}", "shortName")); + p.add(new AbstractPermissionUrlPath.BoardShortNameUrlPath("/card-mapping/{boardShortName:[A-Z0-9_]+}", + "boardShortName:[A-Z0-9_]+")); + p.add(new AbstractPermissionUrlPath.BoardShortNameUrlPath("/card-by-seq/{boardShortName:[A-Z0-9_]+}", + "boardShortName:[A-Z0-9_]+")); + + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/actionitem/{actionItemId}", "actionItemId")); + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/actionlist/{actionListId}", "actionListId")); + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/comment/{commentId}", "commentId")); + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/file/{fileId}", "fileId")); + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/from-actionlist/{from}", "from")); + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/move-to-actionlist/{to}", "to")); + p.add(new AbstractPermissionUrlPath.CardDataIdUrlPath("/activity/{id}", "id")); + + p.add(new AbstractPermissionUrlPath.CardIdUrlPath("/card/{cardId}", "cardId")); + + p.add(new AbstractPermissionUrlPath.ColumnIdUrlPath("/column/{columnId}", "columnId")); + p.add(new AbstractPermissionUrlPath.ColumnIdUrlPath("/from-column/{previousColumnId}", "previousColumnId")); + p.add(new AbstractPermissionUrlPath.ColumnIdUrlPath("/to-column/{newColumnId}", "newColumnId")); + + p.add(new AbstractPermissionUrlPath.EventIdUrlPath("/undo/{eventId}", "eventId")); + + p.add(new AbstractPermissionUrlPath.ProjectShortNameUrlPath("/project/{projectShortName}", "projectShortName")); + + p.add(new AbstractPermissionUrlPath.LabelIdUrlPath("/label/{labelId}", "labelId")); + p.add(new AbstractPermissionUrlPath.LabelValueIdUrlPath("/card-label-value/{labelValueId}", "labelValueId")); + + p.add(new AbstractPermissionUrlPath.ColumnDefinitionIdUrlPath("/redefine/{newDefinitionId}", "newDefinitionId")); + + URL_PATTERNS_TO_CHECK = Collections.unmodifiableSet(p); + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + ExpectPermission expectPermission = ExpectPermission.Helper.getAnnotation(handler); + if (expectPermission == null) { + return true; + } + + Class ownershipChecker = expectPermission.ownershipChecker(); + + UserWithPermission user = UserSession.fetchFromRequest(request, userService); + + // check the base permission + if (user.getBasePermissions().containsKey(expectPermission.value())) { + return true; + } + + // check if the resource has a custom ownership checker (the user must + // have a read permission) + if (NoOpOwnershipChecker.class != ownershipChecker && user.getBasePermissions().containsKey(Permission.READ) + && context.getBean(expectPermission.ownershipChecker()).hasOwnership(request, user)) { + return true; + } + // + + // TODO: in some cases the user pass a list of ids as a _body_. This + // kind of bulk operation will need to ensure the correctness with the + // right filter. + + // project level check + Set projectIds = extractProjectIdsFromRequestUri(request.getRequestURI(), projectService); + if (allProjectsIdsHavePermission(projectIds, user, expectPermission.value())) { + return true; + } + + // project level ownership check + if (NoOpOwnershipChecker.class != ownershipChecker + && allProjectsIdsHavePermission(projectIds, user, Permission.READ) + && context.getBean(expectPermission.ownershipChecker()).hasOwnership(request, user)) { + return true; + } + + response.sendError(HttpStatus.FORBIDDEN.value()); + return false; + } + + private static Set extractProjectIdsFromRequestUri(String requestUri, ProjectService projectService) { + Set projectIds = new HashSet<>(); + for (AbstractPermissionUrlPath p : URL_PATTERNS_TO_CHECK) { + Set extracted = p.tryToFetchProjectShortNames(requestUri, projectService); + projectIds.addAll(extracted); + } + return projectIds; + } + + /*** + * Check that all the related project have the expected permission + * + * @param projectIds + * @param userName + * @param expectedPermission + * @return + */ + private boolean allProjectsIdsHavePermission(Set projectIds, UserWithPermission user, + Permission expectedPermission) { + + if (projectIds.isEmpty()) { + return false; + } + + for (String projectName : projectIds) { + if (!user.getPermissionsForProject().containsKey(projectName) + || !user.getPermissionsForProject().get(projectName).containsKey(expectedPermission)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/io/lavagna/web/helper/Redirector.java b/src/main/java/io/lavagna/web/helper/Redirector.java new file mode 100644 index 000000000..a563c9a65 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/Redirector.java @@ -0,0 +1,69 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.Key; +import io.lavagna.service.ConfigurationRepository; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +public final class Redirector { + + private Redirector() { + } + + public static String cleanupRequestedUrl(String r) { + try { + return (r == null || !r.startsWith("/")) ? "/" : URLDecoder.decode(r, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return r; + } + } + + public static String fetchRequestedUrl(HttpServletRequest req) { + return cleanupRequestedUrl(req.getParameter("reqUrl")); + } + + public static void sendRedirect(HttpServletRequest req, HttpServletResponse resp, String page) throws IOException { + sendRedirect(req, resp, page, Collections.> emptyMap()); + } + + public static void sendRedirect(HttpServletRequest req, HttpServletResponse resp, String page, + Map> params) throws IOException { + WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(req.getServletContext()); + String baseApplicationUrl = ctx.getBean(ConfigurationRepository.class).getValue(Key.BASE_APPLICATION_URL); + + UriComponents urlToRedirect = UriComponentsBuilder.fromHttpUrl(baseApplicationUrl).path(page) + .queryParams(new LinkedMultiValueMap<>(params)).build(); + + resp.sendRedirect(urlToRedirect.toUriString()); + } +} diff --git a/src/main/java/io/lavagna/web/helper/UserArgumentResolver.java b/src/main/java/io/lavagna/web/helper/UserArgumentResolver.java new file mode 100644 index 000000000..460c4b1eb --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/UserArgumentResolver.java @@ -0,0 +1,55 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.User; +import io.lavagna.service.UserService; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class UserArgumentResolver implements HandlerMethodArgumentResolver { + + private final UserService userService; + + @Autowired + public UserArgumentResolver(UserService userService) { + this.userService = userService; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return User.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + return UserSession.fetchFromRequest(webRequest.getNativeRequest(HttpServletRequest.class), userService); + + } + +} diff --git a/src/main/java/io/lavagna/web/helper/UserSession.java b/src/main/java/io/lavagna/web/helper/UserSession.java new file mode 100644 index 000000000..eca574dc7 --- /dev/null +++ b/src/main/java/io/lavagna/web/helper/UserSession.java @@ -0,0 +1,174 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.helper; + +import io.lavagna.model.User; +import io.lavagna.model.UserWithPermission; +import io.lavagna.service.UserRepository; +import io.lavagna.service.UserService; + +import java.util.Objects; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class UserSession { + + private static final Logger LOG = LogManager.getLogger(); + + private UserSession() { + } + + private static final String AUTH_KEY = UserSession.class.getName() + ".AUTH_KEY"; + private static final String AUTH_USER_ID = UserSession.class.getName() + ".AUTH_USER_ID"; + private static final String AUTH_USER_IS_ANONYMOUS = UserSession.class.getName() + ".AUTH_USER_IS_ANONYMOUS"; + + public static boolean isUserAuthenticated(HttpServletRequest req) { + return Boolean.TRUE.equals(req.getSession().getAttribute(AUTH_KEY)); + } + + public static boolean isUserAnonymous(HttpServletRequest req) { + return Boolean.TRUE.equals(req.getSession().getAttribute(AUTH_USER_IS_ANONYMOUS)); + } + + public static void authenticateUserIfRemembered(HttpServletRequest req, HttpServletResponse resp, + UserRepository userRepository) { + Cookie c; + if (isUserAuthenticated(req) || (c = getCookie(req, "LAVAGNA_REMEMBER_ME")) == null) { + return; + } + + ImmutablePair uIdToken = extractUserIdAndToken(c.getValue()); + + if (uIdToken != null && userRepository.rememberMeTokenExists(uIdToken.getLeft(), uIdToken.getRight())) { + userRepository.deleteRememberMeToken(uIdToken.getLeft(), uIdToken.getRight()); + setUser(userRepository.findById(uIdToken.getLeft()), req, resp, userRepository, true); + } else { + // delete cookie because it's invalid + c.setMaxAge(0); + c.setValue(null); + resp.addCookie(c); + } + } + + public static int getUserId(HttpServletRequest req) { + Object o = req.getSession().getAttribute(AUTH_USER_ID); + Objects.requireNonNull(o); + return (int) o; + } + + public static void invalidate(HttpServletRequest req, HttpServletResponse resp, UserRepository userRepository) { + req.getSession().invalidate(); + Cookie c = getCookie(req, "LAVAGNA_REMEMBER_ME"); + if (c != null) { + deleteTokenIfExist(c.getValue(), userRepository); + c.setMaxAge(0); + c.setValue(null); + resp.addCookie(c); + } + } + + private static ImmutablePair extractUserIdAndToken(String cookieVal) { + try { + String[] splitted = cookieVal.split(","); + if (splitted.length == 2) { + int userId = Integer.valueOf(splitted[0], 10); + String token = splitted[1]; + if (token != null) { + return ImmutablePair.of(userId, token); + } + } + } catch (NullPointerException | NumberFormatException e) { + LOG.info("error while extracting userid and token", e); + } + return null; + } + + private static void deleteTokenIfExist(String cookieVal, UserRepository userRepository) { + ImmutablePair uIdToken = extractUserIdAndToken(cookieVal); + if (uIdToken != null) { + userRepository.deleteRememberMeToken(uIdToken.getLeft(), uIdToken.getRight()); + } + } + + private static Cookie getCookie(HttpServletRequest request, String name) { + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + if (cookie.getName().equals(name)) { + return cookie; + } + } + } + + return null; + } + + public static void setUser(User user, HttpServletRequest req, HttpServletResponse resp, + UserRepository userRepository, boolean addRememberMeCookie) { + + req.getSession().invalidate(); + if (addRememberMeCookie) { + addRememberMeCookie(user.getId(), req, resp, userRepository); + } + HttpSession session = req.getSession(true); + session.setAttribute(AUTH_KEY, true); + session.setAttribute(AUTH_USER_ID, user.getId()); + session.setAttribute(AUTH_USER_IS_ANONYMOUS, user.isAnonymous()); + } + + public static void setUser(User user, HttpServletRequest req, HttpServletResponse resp, + UserRepository userRepository) { + boolean rememberMe = "true".equals(req.getParameter("rememberMe")) + || "true".equals(req.getAttribute("rememberMe")); + setUser(user, req, resp, userRepository, rememberMe); + } + + private static void addRememberMeCookie(int userId, HttpServletRequest req, HttpServletResponse resp, + UserRepository userRepository) { + + String token = userRepository.createRememberMeToken(userId); + // + Cookie c = new Cookie("LAVAGNA_REMEMBER_ME", userId + "," + token); + c.setPath(req.getContextPath() + "/"); + c.setHttpOnly(true); + c.setMaxAge(60 * 60 * 24 * 365); // 1 year + if (req.getServletContext().getSessionCookieConfig().isSecure()) { + c.setSecure(true); + } + resp.addCookie(c); + } + + static UserWithPermission fetchFromRequest(HttpServletRequest request, UserService userService) { + + Object userAttr = request.getAttribute(UserWithPermission.class.getName()); + + if (userAttr != null) { + return (UserWithPermission) userAttr; + } else { + UserWithPermission res = userService.findUserWithPermission(getUserId(request)); + request.setAttribute(UserWithPermission.class.getName(), res); + return res; + } + } + +} diff --git a/src/main/java/io/lavagna/web/security/PathConfiguration.java b/src/main/java/io/lavagna/web/security/PathConfiguration.java new file mode 100644 index 000000000..be1c163cf --- /dev/null +++ b/src/main/java/io/lavagna/web/security/PathConfiguration.java @@ -0,0 +1,291 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security; + +import static io.lavagna.web.security.login.LoginHandler.AbstractLoginHandler.logout; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.apache.commons.lang3.StringUtils.removeStart; +import static org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext; +import io.lavagna.common.Json; +import io.lavagna.model.Key; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.Redirector; +import io.lavagna.web.helper.UserSession; +import io.lavagna.web.security.login.DemoLogin; +import io.lavagna.web.security.login.LdapLogin; +import io.lavagna.web.security.login.LoginHandler; +import io.lavagna.web.security.login.OAuthLogin; +import io.lavagna.web.security.login.PersonaLogin; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.springframework.util.PathMatcher; +import org.springframework.web.context.WebApplicationContext; + +import com.samskivert.mustache.Mustache; + +public class PathConfiguration { + + private final List urlMatchers = new ArrayList<>(); + private boolean loginUrlDisabled; + private LoginUrlMatcher loginUrlMatcher; + private LogoutUrlMatcher logoutUrlMatcher; + + List buildMatcherList() { + List r = new ArrayList<>(); + + if (!loginUrlDisabled) { + Objects.requireNonNull(loginUrlMatcher, "login urls must be configured or disabled"); + Objects.requireNonNull(logoutUrlMatcher, "logout urls must be configured or disabled"); + + r.add(loginUrlMatcher); + r.add(logoutUrlMatcher); + } + + r.addAll(urlMatchers); + return r; + } + + public PathConfiguration disableLogin() { + loginUrlDisabled = true; + return this; + } + + // + public LogoutConfigurer login(String loginUrlMatcher, String loginPageUrl, String loginPage) { + Validate.isTrue(!loginUrlDisabled, "login has been disabled"); + this.loginUrlMatcher = new LoginUrlMatcher(loginUrlMatcher, loginPageUrl, loginPage); + return new LogoutConfigurer(this); + } + + public BasicUrlMatcher request(String url) { + BasicUrlMatcher urlMatcher = new BasicUrlMatcher(this, url); + urlMatchers.add(urlMatcher); + return urlMatcher; + } + + public static class LogoutConfigurer { + + private final PathConfiguration conf; + + private LogoutConfigurer(PathConfiguration conf) { + this.conf = conf; + } + + public PathConfiguration logout(String logoutUrlMatcher, String logoutBaseUrl) { + conf.logoutUrlMatcher = new LogoutUrlMatcher(logoutUrlMatcher, logoutBaseUrl); + return conf; + } + } + + public interface UrlMatcher { + boolean match(String url, PathMatcher pathMatcher); + + boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException; + } + + public static class BasicUrlMatcher implements UrlMatcher { + private final String urlMatcher; + private final PathConfiguration conf; + private boolean redirect; + private Mode mode; + + BasicUrlMatcher(PathConfiguration conf, String urlMatcher) { + this.conf = conf; + this.urlMatcher = urlMatcher; + } + + public PathConfiguration denyAll() { + mode = Mode.DENY_ALL; + return conf; + } + + public PathConfiguration requireAuthenticated() { + return requireAuthenticated(true); + } + + public PathConfiguration requireAuthenticated(boolean redirect) { + mode = Mode.REQUIRE_AUTHENTICATED; + this.redirect = redirect; + return conf; + } + + public PathConfiguration permitAll() { + mode = Mode.PERMIT_ALL; + return conf; + } + + @Override + public boolean match(String url, PathMatcher pathMatcher) { + return pathMatcher.match(urlMatcher, url); + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (mode == Mode.REQUIRE_AUTHENTICATED && !UserSession.isUserAuthenticated(req)) { + if (redirect) { + String requestedUrl = extractRequestedUrl(req); + Redirector.sendRedirect(req, resp, conf.loginUrlMatcher.loginPageUrl, + singletonMap("reqUrl", singletonList(URLEncoder.encode(requestedUrl, "UTF-8")))); + } else { + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + return true; + } else if (mode == Mode.DENY_ALL) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return true; + } else { + return false; + } + } + } + + private static String extractRequestedUrl(HttpServletRequest req) { + String queryString = req.getQueryString(); + return removeStart(req.getRequestURI(), req.getContextPath()) + + (queryString != null ? ("?" + queryString) : ""); + } + + public static class LogoutUrlMatcher implements UrlMatcher { + private final String logoutUrlMatcher; + private final String logoutBaseUrl; + + LogoutUrlMatcher(String logoutUrlMatcher, String logoutBaseUrl) { + this.logoutBaseUrl = logoutBaseUrl; + this.logoutUrlMatcher = logoutUrlMatcher; + } + + @Override + public boolean match(String url, PathMatcher pathMatcher) { + return pathMatcher.match(logoutUrlMatcher, url); + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + Map handlers = findActiveLoginHandlers(req); + String subPath = findSubpath(req, logoutBaseUrl); + if (handlers.containsKey(subPath)) { + return handlers.get(subPath).handleLogout(req, resp); + } else { + // fallback to default logout handler + return logout(req, resp, getWebApplicationContext(req.getServletContext()) + .getBean(UserRepository.class)); + } + } + } + + public static class LoginUrlMatcher implements UrlMatcher { + private final String urlMatcher; + private final String loginPageUrl; + private final String loginPage; + + LoginUrlMatcher(String urlMatcher, String loginPageUrl, String loginPage) { + this.urlMatcher = urlMatcher; + this.loginPageUrl = loginPageUrl; + this.loginPage = loginPage; + } + + @Override + public boolean match(String url, PathMatcher pathMatcher) { + return pathMatcher.match(urlMatcher, url); + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + Map handlers = findActiveLoginHandlers(req); + + // handle the static page for the login, we expect that it's a GET + // request _and_ it match the configured path (/login) + if ("GET".equalsIgnoreCase(req.getMethod()) && loginPageUrl.equals(req.getServletPath())) { + InputStream is = req.getServletContext().getResourceAsStream(loginPage); + Map model = new HashMap<>(); + for (LoginHandler lh : handlers.values()) { + model.putAll(lh.modelForLoginPage(req)); + } + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/html"); + Mustache.compiler().defaultValue("").compile(new InputStreamReader(is, StandardCharsets.UTF_8)) + .execute(model, resp.getWriter()); + return true; + } + // ------------------------------- + // given /login/demo/ -> return demo + // subPath will be demo/ldap/oauth/persona + String subPath = findSubpath(req, loginPageUrl); + + if (handlers.containsKey(subPath)) { + return handlers.get(subPath).doAction(req, resp); + } else { + return false; + } + } + + } + + // given /login/demo/ -> return demo + private static String findSubpath(HttpServletRequest req, String firstPath) { + return StringUtils.substringBefore(StringUtils.substring(req.getServletPath(), firstPath.length() + 1), "/"); + } + + private static Map findActiveLoginHandlers(HttpServletRequest req) { + Map res = new HashMap(); + WebApplicationContext ctx = getWebApplicationContext(req.getServletContext()); + LoginHandlerType[] authMethods = Json.GSON.fromJson( + ctx.getBean(ConfigurationRepository.class).getValue(Key.AUTHENTICATION_METHOD), + LoginHandlerType[].class); + for (LoginHandlerType m : authMethods) { + res.put(m.pathAfterLogin, ctx.getBean(m.classHandler)); + } + return res; + } + + public enum LoginHandlerType { + + DEMO(DemoLogin.class, "demo"), LDAP(LdapLogin.class, "ldap"), OAUTH(OAuthLogin.class, "oauth"), PERSONA( + PersonaLogin.class, "persona"); + + LoginHandlerType(Class classHandler, String pathAfterLogin) { + this.classHandler = classHandler; + this.pathAfterLogin = pathAfterLogin; + } + + private final Class classHandler; + private final String pathAfterLogin; + + } + + private enum Mode { + DENY_ALL, UNAUTHENTICATED, REQUIRE_AUTHENTICATED, PERMIT_ALL, LOGIN, LOGOUT + } +} diff --git a/src/main/java/io/lavagna/web/security/SecurityFilter.java b/src/main/java/io/lavagna/web/security/SecurityFilter.java new file mode 100644 index 000000000..6af8f751a --- /dev/null +++ b/src/main/java/io/lavagna/web/security/SecurityFilter.java @@ -0,0 +1,318 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security; + +import static java.util.EnumSet.of; +import static org.springframework.web.context.support.WebApplicationContextUtils.getRequiredWebApplicationContext; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.context.WebApplicationContext; + +import io.lavagna.model.Key; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.CSRFToken; +import io.lavagna.web.helper.Redirector; +import io.lavagna.web.helper.UserSession; +import io.lavagna.web.security.PathConfiguration.UrlMatcher; + +/** + *
+ * FIXME: obviously I'm not happy with this one... 
+ * 
+ * - I was not able to do a dynamic configuration with spring security.
+ * - I needed some additional control
+ * 
+ * If there is some kind of alternatives that give me the same features/functionality as the current version...
+ * 
+ */ +public class SecurityFilter implements Filter { + + private static final Logger LOG = LogManager.getLogger(); + + private final PathMatcher pathMatcher = new AntPathMatcher(); + private ConfigurationRepository config; + private UserRepository userRepository; + private List configuredAppPathConf; + private List unconfiguredAppPathConf; + + // + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + WebApplicationContext ctx = getRequiredWebApplicationContext(filterConfig.getServletContext()); + config = ctx.getBean(ConfigurationRepository.class); + userRepository = ctx.getBean(UserRepository.class); + + configuredAppPathConf = ctx.getBean("configuredAppPathConf", PathConfiguration.class).buildMatcherList(); + unconfiguredAppPathConf = ctx.getBean("unconfiguredAppPathConf", PathConfiguration.class).buildMatcherList(); + + if ("true".equals(config.getValueOrNull(Key.USE_HTTPS))) { + filterConfig.getServletContext().getSessionCookieConfig().setSecure(true); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + // + + String reqURI = req.getRequestURI(); + + // if it's not in the context path of the application, the security + // filter will not be triggered + if (!reqURI.startsWith(req.getServletContext().getContextPath())) { + chain.doFilter(request, response); + return; + } + + Map configuration = config.findConfigurationFor(of(Key.SETUP_COMPLETE, Key.USE_HTTPS, + Key.BASE_APPLICATION_URL, Key.ENABLE_ANON_USER)); + + if (!handleHttps(req, resp, configuration)) { + return; + } + + if (!handleCSRF(req, resp)) { + return; + } + + addHeaders(req, resp); + + // handle with the correct Url matcher list... + if ("true".equals(configuration.get(Key.SETUP_COMPLETE))) { + handleRememberMe(req, resp); + + handleAnonymousUser(configuration, req, resp); + + handleWith(req, resp, chain, configuredAppPathConf); + } else { + handleWith(req, resp, chain, unconfiguredAppPathConf); + } + } + + private void handleAnonymousUser(Map configuration, HttpServletRequest req, HttpServletResponse resp) { + + final boolean enabled = "true".equals(configuration.get(Key.ENABLE_ANON_USER)); + + if (enabled && !UserSession.isUserAuthenticated(req)) { + UserSession.setUser(userRepository.findUserByName("system", "anonymous"), req, resp, userRepository, false); + } + + // handle the case when the user is logged as a anonymous user but it's + // no more enabled + if (!enabled && UserSession.isUserAuthenticated(req) && UserSession.isUserAnonymous(req)) { + UserSession.invalidate(req, resp, userRepository); + } + } + + private void handleRememberMe(HttpServletRequest req, HttpServletResponse resp) { + UserSession.authenticateUserIfRemembered(req, resp, userRepository); + } + + /** + * Return true if a redirect has been sent and thus the whole flow must be stopped. + * + * @param req + * @param resp + * @param configuration + * @return + * @throws IOException + */ + private boolean handleHttps(HttpServletRequest req, HttpServletResponse resp, Map configuration) + throws IOException { + + final boolean requestOverHttps = isOverHttps(req); + final boolean useHttps = "true".equals(configuration.get(Key.USE_HTTPS)); + + // warn if the configuration is not aligned with the runtime settings + boolean hasConfProblem = false; + if (req.getServletContext().getSessionCookieConfig().isSecure() != useHttps) { + LOG.warn("SessionCookieConfig is not aligned with settings. The application must be restarted."); + hasConfProblem = true; + } + if (useHttps && !configuration.get(Key.BASE_APPLICATION_URL).startsWith("https://")) { + LOG.warn( + "The base application url {} does not begin with https:// . It's a mandatory requirement if you want to enable full https mode.", + configuration.get(Key.BASE_APPLICATION_URL)); + hasConfProblem = hasConfProblem || true; + } + + // IF ANY CONF error, will skip this part + if (hasConfProblem) { + return true; + } + + String reqUriWithoutContextPath = reqUriWithoutContextPath(req); + + // TODO: we ignore the websocket because the openshift websocket proxy + // does not add the X-Forwarded-Proto header. : -> no redirection and + // STS for the calls under /api/socket/* + + if (useHttps && !requestOverHttps && !reqUriWithoutContextPath.startsWith("/api/socket/")) { + LOG.debug("use https is true and request is not over https, should redirect request"); + Redirector.sendRedirect(req, resp, reqUriWithoutContextPath); + return false; + } else if (useHttps && requestOverHttps) { + LOG.debug("use https is true and request is over https, adding STS header"); + resp.setHeader("Strict-Transport-Security", "max-age=31536000"); + return true; + } + return true; + } + + @Override + public void destroy() { + } + + /** + * Return false if there is an error + * + * @param request + * @param response + * @return + * @throws IOException + */ + private boolean handleCSRF(HttpServletRequest request, HttpServletResponse response) throws IOException { + String token = (String) request.getSession().getAttribute(CSRFToken.CSRF_TOKEN); + if (token == null) { + token = UUID.randomUUID().toString(); + request.getSession().setAttribute(CSRFToken.CSRF_TOKEN, token); + } + response.setHeader(CSRFToken.CSRF_TOKEN_HEADER, token); + // + if (mustCheckCSRF(request)) { + return checkCSRF(request, response); + } + // + return true; + } + + private static final Pattern WEBSOCKET_FALLBACK = Pattern.compile("^/api/socket/.*$"); + + /** + * Return true if the filter must check the + * + * @param request + * @return + */ + private boolean mustCheckCSRF(HttpServletRequest request) { + + // ignore the websocket fallback... + if ("POST".equals(request.getMethod()) + && WEBSOCKET_FALLBACK.matcher(request.getContextPath() + request.getRequestURI()).matches()) { + return false; + } + + return !CSRFToken.CSRF_METHOD_DONT_CHECK.matcher(request.getMethod()).matches(); + } + + private static boolean checkCSRF(HttpServletRequest request, HttpServletResponse response) throws IOException { + String expectedToken = (String) request.getSession().getAttribute(CSRFToken.CSRF_TOKEN); + String token = request.getHeader(CSRFToken.CSRF_TOKEN_HEADER); + if (token == null) { + token = request.getParameter(CSRFToken.CSRF_FORM_PARAMETER); + } + + if (token == null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "missing token in header or parameter"); + return false; + } + if (expectedToken == null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "missing token from session"); + return false; + } + if (!CSRFToken.safeArrayEquals(token.getBytes("UTF-8"), expectedToken.getBytes("UTF-8"))) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "token is not equal to expected"); + return false; + } + + return true; + } + + private void handleWith(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, + List matchers) throws IOException, ServletException { + + String reqUriWithoutContextPath = reqUriWithoutContextPath(request); + + for (UrlMatcher urlMatcher : matchers) { + if (urlMatcher.match(reqUriWithoutContextPath, pathMatcher)) { + if (urlMatcher.doAction(request, response)) { + // the action has been handled by the url matcher, no + // further processing is required + return; + } else { + break; + } + } + } + + filterChain.doFilter(request, response); + } + + private static String reqUriWithoutContextPath(HttpServletRequest request) { + return request.getRequestURI().substring(request.getServletContext().getContextPath().length()); + } + + private static boolean canApplyNoCachingHeaders(HttpServletRequest req) { + String u = req.getRequestURI(); + return !("/".equals(u) || u.matches(".*\\.(css|gif|js|png|html|eot|svg|ttf|woff)$")); + } + + /** + */ + // TODO check if the no caching directives could be removed/or at least + // changed... (?) + private static void addHeaders(HttpServletRequest req, HttpServletResponse res) { + if (canApplyNoCachingHeaders(req)) { + res.addHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + res.addHeader("Expires", "0"); + res.addHeader("Pragma", "no-cache"); + } + + res.addHeader("X-Frame-Options", "DENY"); + res.addHeader("X-XSS-Protection", "1; mode=block"); + res.addHeader("x-content-type-options", "nosniff"); + } + + private static boolean isOverHttps(HttpServletRequest req) { + return req.isSecure() || req.getRequestURL().toString().startsWith("https://") + || StringUtils.equals("https", req.getHeader("X-Forwarded-Proto")); + } +} diff --git a/src/main/java/io/lavagna/web/security/login/DemoLogin.java b/src/main/java/io/lavagna/web/security/login/DemoLogin.java new file mode 100644 index 000000000..3c91b5fc5 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/DemoLogin.java @@ -0,0 +1,69 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login; + +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.Redirector; +import io.lavagna.web.helper.UserSession; +import io.lavagna.web.security.login.LoginHandler.AbstractLoginHandler; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class DemoLogin extends AbstractLoginHandler { + + static final String USER_PROVIDER = "demo"; + + private final String errorPage; + + public DemoLogin(UserRepository userRepository, String errorPage) { + super(userRepository); + this.errorPage = errorPage; + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (!"POST".equalsIgnoreCase(req.getMethod())) { + return false; + } + + String username = req.getParameter("username"); + String password = req.getParameter("password"); + // yes, it's stupid... + if (username != null && username.equals(password) + && userRepository.userExistsAndEnabled(USER_PROVIDER, username)) { + // FIXME refactor out + String url = Redirector.fetchRequestedUrl(req); + UserSession.setUser(userRepository.findUserByName(USER_PROVIDER, username), req, resp, userRepository); + Redirector.sendRedirect(req, resp, url); + } else { + Redirector.sendRedirect(req, resp, errorPage); + } + return true; + } + + @Override + public Map modelForLoginPage(HttpServletRequest request) { + Map r = super.modelForLoginPage(request); + r.put("loginDemo", "block"); + return r; + } +} diff --git a/src/main/java/io/lavagna/web/security/login/LdapLogin.java b/src/main/java/io/lavagna/web/security/login/LdapLogin.java new file mode 100644 index 000000000..f78969811 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/LdapLogin.java @@ -0,0 +1,84 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login; + +import io.lavagna.service.Ldap; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.Redirector; +import io.lavagna.web.helper.UserSession; +import io.lavagna.web.security.login.LoginHandler.AbstractLoginHandler; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; + +public class LdapLogin extends AbstractLoginHandler { + + static final String USER_PROVIDER = "ldap"; + + private final String errorPage; + private final Ldap ldap; + + public LdapLogin(UserRepository userRepository, Ldap ldap, String errorPage) { + super(userRepository); + this.ldap = ldap; + this.errorPage = errorPage; + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (!"POST".equalsIgnoreCase(req.getMethod())) { + return false; + } + + String username = req.getParameter("username"); + String password = req.getParameter("password"); + + if (authenticate(username, password)) { + // FIXME refactor out + String url = Redirector.fetchRequestedUrl(req); + UserSession.setUser(userRepository.findUserByName(USER_PROVIDER, username), req, resp, userRepository); + Redirector.sendRedirect(req, resp, url); + } else { + Redirector.sendRedirect(req, resp, errorPage); + } + return true; + + } + + private boolean authenticate(String username, String password) { + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password) + || !userRepository.userExistsAndEnabled(USER_PROVIDER, username)) { + return false; + } + + return ldap.authenticate(username, password); + } + + @Override + public Map modelForLoginPage(HttpServletRequest request) { + Map r = super.modelForLoginPage(request); + r.put("loginLdap", "block"); + return r; + } + +} diff --git a/src/main/java/io/lavagna/web/security/login/LoginHandler.java b/src/main/java/io/lavagna/web/security/login/LoginHandler.java new file mode 100644 index 000000000..b20124591 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/LoginHandler.java @@ -0,0 +1,71 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login; + +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.CSRFToken; +import io.lavagna.web.helper.UserSession; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.util.UriComponentsBuilder; + +public interface LoginHandler { + + boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException; + + boolean handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException; + + Map modelForLoginPage(HttpServletRequest request); + + abstract class AbstractLoginHandler implements LoginHandler { + + protected final UserRepository userRepository; + + AbstractLoginHandler(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public static boolean logout(HttpServletRequest req, HttpServletResponse resp, UserRepository userRepository) + throws IOException, ServletException { + UserSession.invalidate(req, resp, userRepository); + resp.setStatus(HttpServletResponse.SC_OK); + return true; + } + + @Override + public boolean handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + return logout(req, resp, userRepository); + } + + public Map modelForLoginPage(HttpServletRequest request) { + String tokenValue = (String) request.getSession().getAttribute(CSRFToken.CSRF_TOKEN); + Map r = new HashMap<>(); + r.put("csrfToken", tokenValue); + r.put("reqUrl", UriComponentsBuilder.fromPath(request.getParameter("reqUrl")).build().encode() + .toUriString()); + return r; + } + } +} diff --git a/src/main/java/io/lavagna/web/security/login/OAuthLogin.java b/src/main/java/io/lavagna/web/security/login/OAuthLogin.java new file mode 100644 index 000000000..cff285df2 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/OAuthLogin.java @@ -0,0 +1,189 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login; + +import io.lavagna.common.Json; +import io.lavagna.model.Key; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.UserRepository; +import io.lavagna.web.security.login.LoginHandler.AbstractLoginHandler; +import io.lavagna.web.security.login.oauth.BitbucketHandler; +import io.lavagna.web.security.login.oauth.GithubHandler; +import io.lavagna.web.security.login.oauth.GoogleHandler; +import io.lavagna.web.security.login.oauth.OAuthResultHandler; +import io.lavagna.web.security.login.oauth.OAuthResultHandler.OAuthRequestBuilder; +import io.lavagna.web.security.login.oauth.TwitterHandler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.reflect.ConstructorUtils; +import org.scribe.builder.ServiceBuilder; +import org.springframework.util.StringUtils; + +public class OAuthLogin extends AbstractLoginHandler { + + static final Map> SUPPORTED_OAUTH_HANDLER; + + static { + Map> r = new LinkedHashMap<>(); + r.put("bitbucket", BitbucketHandler.class); + r.put("github", GithubHandler.class); + r.put("google", GoogleHandler.class); + r.put("twitter", TwitterHandler.class); + SUPPORTED_OAUTH_HANDLER = Collections.unmodifiableMap(r); + } + + private final ConfigurationRepository configurationRepository; + private final String errorPage; + private final Handler handler; + + public OAuthLogin(UserRepository userRepository, ConfigurationRepository configurationRepository, Handler handler, + String errorPage) { + super(userRepository); + this.configurationRepository = configurationRepository; + this.errorPage = errorPage; + this.handler = handler; + + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + OAuthConfiguration conf = Json.GSON.fromJson(configurationRepository.getValue(Key.OAUTH_CONFIGURATION), + OAuthConfiguration.class); + + String requestURI = req.getRequestURI(); + + if ("POST".equals(req.getMethod())) { + OAuthProvider authHandler = conf.matchAuthorization(requestURI); + if (authHandler != null) { + handler.from(authHandler, conf.baseUrl, userRepository, errorPage).handleAuthorizationUrl(req, resp); + return true; + } + } + + OAuthProvider callbackHandler = conf.matchCallback(requestURI); + if (callbackHandler != null) { + handler.from(callbackHandler, conf.baseUrl, userRepository, errorPage).handleCallback(req, resp); + return true; + } + return false; + } + + @Override + public Map modelForLoginPage(HttpServletRequest request) { + + Map m = super.modelForLoginPage(request); + + OAuthConfiguration conf = Json.GSON.fromJson(configurationRepository.getValue(Key.OAUTH_CONFIGURATION), + OAuthConfiguration.class); + + List loginOauthProviders = new ArrayList<>(); + + for (String p : SUPPORTED_OAUTH_HANDLER.keySet()) { + if (conf.hasProvider(p)) { + loginOauthProviders.add(p); + } + } + m.put("loginOauthProviders", loginOauthProviders); + m.put("loginOauth", "block"); + + return m; + } + + static class OAuthConfiguration { + String baseUrl; + List providers; + + public boolean hasProvider(String provider) { + for (OAuthProvider o : providers) { + if (provider.equals(o.provider)) { + return true; + } + } + return false; + } + + public OAuthProvider matchAuthorization(String requestURI) { + for (OAuthProvider o : providers) { + if (o.matchAuthorization(requestURI)) { + return o; + } + } + return null; + } + + public OAuthProvider matchCallback(String requestURI) { + for (OAuthProvider o : providers) { + if (o.matchCallback(requestURI)) { + return o; + } + } + return null; + } + } + + public static class Handler { + + private final ServiceBuilder serviceBuilder; + private final OAuthRequestBuilder reqBuilder = new OAuthRequestBuilder(); + + public Handler(ServiceBuilder serviceBuilder) { + this.serviceBuilder = serviceBuilder; + } + + // TODO: refactor + public OAuthResultHandler from(OAuthProvider oauthProvider, String confBaseUrl, UserRepository userRepository, + String errorPage) { + String baseUrl = StringUtils.trimTrailingCharacter(confBaseUrl, '/'); + String callbackUrl = baseUrl + "/login/oauth/" + oauthProvider.provider + "/callback"; + if (SUPPORTED_OAUTH_HANDLER.containsKey(oauthProvider.provider)) { + try { + return ConstructorUtils.invokeConstructor(SUPPORTED_OAUTH_HANDLER.get(oauthProvider.provider), + serviceBuilder, reqBuilder, oauthProvider.apiKey, oauthProvider.apiSecret, callbackUrl, + userRepository, errorPage); + } catch (ReflectiveOperationException iea) { + throw new IllegalStateException(iea); + } + } else { + throw new IllegalArgumentException("type " + oauthProvider.provider + " is not supported"); + } + } + } + + static class OAuthProvider { + String provider;// google, github, bitbucket, twitter + String apiKey; + String apiSecret; + + public boolean matchAuthorization(String requestURI) { + return requestURI.endsWith("login/oauth/" + provider); + } + + public boolean matchCallback(String requestURI) { + return requestURI.endsWith("login/oauth/" + provider + "/callback"); + } + } +} diff --git a/src/main/java/io/lavagna/web/security/login/PersonaLogin.java b/src/main/java/io/lavagna/web/security/login/PersonaLogin.java new file mode 100644 index 000000000..7c5b8de53 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/PersonaLogin.java @@ -0,0 +1,163 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login; + +import io.lavagna.model.Key; +import io.lavagna.service.ConfigurationRepository; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.Redirector; +import io.lavagna.web.helper.UserSession; +import io.lavagna.web.security.login.LoginHandler.AbstractLoginHandler; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * Implementation notes: + * + *
+ * - https://developer.mozilla.org/en-US/Persona/Quick_Setup
+ * - https://github.com/mozilla/browserid-cookbook/blob/master/java/spring/src/pt/webdetails/browserid/BrowserIdVerifier.java
+ * - https://developer.mozilla.org/en-US/Persona/Security_Considerations
+ * 
+ * + * response object: + * + *
+ * {
+ *   "status": "okay",
+ *   "email": "bob@eyedee.me",
+ *   "audience": "https://example.com:443",
+ *   "expires": 1308859352261,
+ *   "issuer": "eyedee.me"
+ * }
+ * 
+ */ +public class PersonaLogin extends AbstractLoginHandler { + + static final String USER_PROVIDER = "persona"; + + private final ConfigurationRepository configurationRepository; + private final RestTemplate restTemplate; + private final String logoutPage; + + public PersonaLogin(UserRepository userRepository, ConfigurationRepository configurationRepository, + RestTemplate restTemplate, String logoutPage) { + + super(userRepository); + + this.configurationRepository = configurationRepository; + this.logoutPage = logoutPage; + this.restTemplate = restTemplate; + } + + @Override + public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (!("POST".equalsIgnoreCase(req.getMethod()) && req.getParameterMap().containsKey("assertion"))) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return true; + } + + String audience = configurationRepository.getValue(Key.PERSONA_AUDIENCE); + + MultiValueMap toPost = new LinkedMultiValueMap<>(); + toPost.add("assertion", req.getParameter("assertion")); + toPost.add("audience", audience); + VerifierResponse verifier = restTemplate.postForObject("https://verifier.login.persona.org/verify", toPost, + VerifierResponse.class); + + if ("okay".equals(verifier.status) && audience.equals(verifier.audience) + && userRepository.userExistsAndEnabled(USER_PROVIDER, verifier.email)) { + String url = Redirector.cleanupRequestedUrl(req.getParameter("reqUrl")); + UserSession + .setUser(userRepository.findUserByName(USER_PROVIDER, verifier.email), req, resp, userRepository); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + JsonObject jsonObject = new JsonObject(); + jsonObject.add("redirectTo", new JsonPrimitive(url)); + resp.getWriter().write(jsonObject.toString()); + } else { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + return true; + } + + static class VerifierResponse { + private String status; + private String email; + private String audience; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getAudience() { + return audience; + } + + public void setAudience(String audience) { + this.audience = audience; + } + } + + @Override + public boolean handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + if ("POST".equalsIgnoreCase(req.getMethod())) { + UserSession.invalidate(req, resp, userRepository); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + JsonObject jsonObject = new JsonObject(); + jsonObject.add("redirectToSelf", new JsonPrimitive(true)); + resp.getWriter().write(jsonObject.toString()); + } else { + req.getRequestDispatcher(logoutPage).forward(req, resp); + } + return true; + } + + @Override + public Map modelForLoginPage(HttpServletRequest request) { + Map r = super.modelForLoginPage(request); + r.put("loginPersona", "block"); + return r; + } + +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/AbstractOAuth1Handler.java b/src/main/java/io/lavagna/web/security/login/oauth/AbstractOAuth1Handler.java new file mode 100644 index 000000000..9a2287777 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/AbstractOAuth1Handler.java @@ -0,0 +1,63 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import java.io.IOException; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.scribe.model.Token; +import org.scribe.oauth.OAuthService; + +import io.lavagna.service.UserRepository; +import io.lavagna.web.security.login.oauth.OAuthResultHandler.OAuthResultHandlerAdapter; + +public abstract class AbstractOAuth1Handler extends OAuthResultHandlerAdapter { + + AbstractOAuth1Handler(String provider, String profileUrl, Class profileClass, + String verifierParamName, UserRepository userRepository, String errorPage, OAuthService oauthService, + OAuthRequestBuilder reqBuilder) { + super(provider, profileUrl, profileClass, verifierParamName, userRepository, errorPage, oauthService, + reqBuilder); + } + + // ignore state parameter as it's not present + @Override + protected boolean validateStateParam(HttpServletRequest req) { + return true; + } + + @Override + public void handleAuthorizationUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + String state = UUID.randomUUID().toString(); + saveStateAndRequestUrlParameter(req, state); + + Token reqToken = oauthService.getRequestToken(); + req.getSession().setAttribute(getClass().getName(), reqToken); + resp.sendRedirect(oauthService.getAuthorizationUrl(reqToken)); + } + + @Override + protected Token reqToken(HttpServletRequest req) { + Token reqToken = (Token) req.getSession().getAttribute(getClass().getName()); + req.getSession().removeAttribute(getClass().getName()); + return reqToken; + } +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/Bitbucket10Api.java b/src/main/java/io/lavagna/web/security/login/oauth/Bitbucket10Api.java new file mode 100644 index 000000000..009465ed6 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/Bitbucket10Api.java @@ -0,0 +1,43 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import org.scribe.builder.api.DefaultApi10a; +import org.scribe.model.Token; + +/** + *
+ * https://confluence.atlassian.com/display/BITBUCKET/OAuth+on+Bitbucket
+ * 
+ */ +class Bitbucket10Api extends DefaultApi10a { + + @Override + public String getRequestTokenEndpoint() { + return "https://bitbucket.org/api/1.0/oauth/request_token"; + } + + @Override + public String getAccessTokenEndpoint() { + return "https://bitbucket.org/api/1.0/oauth/access_token"; + } + + @Override + public String getAuthorizationUrl(Token requestToken) { + return "https://bitbucket.org/api/1.0/oauth/authenticate?oauth_token=" + requestToken.getToken(); + } +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/BitbucketHandler.java b/src/main/java/io/lavagna/web/security/login/oauth/BitbucketHandler.java new file mode 100644 index 000000000..7386392ae --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/BitbucketHandler.java @@ -0,0 +1,54 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import io.lavagna.service.UserRepository; + +import org.scribe.builder.ServiceBuilder; + +public class BitbucketHandler extends AbstractOAuth1Handler { + + public BitbucketHandler(ServiceBuilder serviceBuilder, OAuthRequestBuilder reqBuilder, String apiKey, + String apiSecret, String callback, UserRepository userRepository, String errorPage) { + super("oauth.bitbucket",// + "https://bitbucket.org/api/1.0/user",// + UserInfo.class, "oauth_verifier", // + userRepository,// + errorPage,// + serviceBuilder.provider(new Bitbucket10Api()).apiKey(apiKey).apiSecret(apiSecret).callback(callback) + .build(), reqBuilder); + } + + private static class UserInfo implements RemoteUserProfile { + + User user; + + @Override + public boolean valid(UserRepository userRepository, String provider) { + return userRepository.userExistsAndEnabled(provider, user.username); + } + + @Override + public String username() { + return user.username; + } + } + + private static class User { + String username; + } +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/Github20Api.java b/src/main/java/io/lavagna/web/security/login/oauth/Github20Api.java new file mode 100644 index 000000000..de9a2abd2 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/Github20Api.java @@ -0,0 +1,36 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import static io.lavagna.web.security.login.oauth.Utils.encode; + +import org.scribe.builder.api.DefaultApi20; +import org.scribe.model.OAuthConfig; + +class Github20Api extends DefaultApi20 { + + @Override + public String getAccessTokenEndpoint() { + return "https://github.com/login/oauth/access_token"; + } + + @Override + public String getAuthorizationUrl(OAuthConfig config) { + return "https://github.com/login/oauth/authorize?client_id=" + encode(config.getApiKey()) + "&redirect_uri=" + + encode(config.getCallback()); + } +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/GithubHandler.java b/src/main/java/io/lavagna/web/security/login/oauth/GithubHandler.java new file mode 100644 index 000000000..6c992c3f6 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/GithubHandler.java @@ -0,0 +1,51 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import io.lavagna.service.UserRepository; +import io.lavagna.web.security.login.oauth.OAuthResultHandler.OAuthResultHandlerAdapter; + +import org.scribe.builder.ServiceBuilder; + +public class GithubHandler extends OAuthResultHandlerAdapter { + + public GithubHandler(ServiceBuilder serviceBuilder, OAuthRequestBuilder reqBuilder, String apiKey, + String apiSecret, String callback, UserRepository userRepository, String errorPage) { + super("oauth.github",// + "https://api.github.com/user",// + UserInfo.class, "code",// + userRepository,// + errorPage,// + serviceBuilder.provider(new Github20Api()).apiKey(apiKey).apiSecret(apiSecret).callback(callback) + .build(), reqBuilder); + } + + private static class UserInfo implements RemoteUserProfile { + String login; + + @Override + public boolean valid(UserRepository userRepository, String provider) { + return userRepository.userExistsAndEnabled(provider, login); + } + + @Override + public String username() { + return login; + } + } + +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/Google20Api.java b/src/main/java/io/lavagna/web/security/login/oauth/Google20Api.java new file mode 100644 index 000000000..a150ecb37 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/Google20Api.java @@ -0,0 +1,124 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import static io.lavagna.web.security.login.oauth.Utils.encode; +import static java.lang.String.format; +import io.lavagna.common.Json; + +import org.apache.commons.lang3.Validate; +import org.scribe.builder.api.DefaultApi20; +import org.scribe.exceptions.OAuthException; +import org.scribe.extractors.AccessTokenExtractor; +import org.scribe.model.OAuthConfig; +import org.scribe.model.OAuthConstants; +import org.scribe.model.OAuthRequest; +import org.scribe.model.Response; +import org.scribe.model.Token; +import org.scribe.model.Verb; +import org.scribe.model.Verifier; +import org.scribe.oauth.OAuth20ServiceImpl; +import org.scribe.oauth.OAuthService; +import org.scribe.utils.Preconditions; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; + +/** + * Google OAuth 2.0 implementation. + * + * Initial form taken from : + * + *
+ * http://svn.codehaus.org/tynamo/tags/tynamo-federatedaccounts-parent-0.3.0/tynamo-federatedaccounts-scribebasedoauth/src/main/java/org/tynamo/security/federatedaccounts/scribe/google/Google20Api.java
+ * 
+ * + * Tynamo is under apache2.0 license. + * + * And then duly modified. + */ +class Google20Api extends DefaultApi20 { + + private static final String AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth?client_id=%s&redirect_uri=%s&response_type=code&scope=%s"; + + @Override + public String getAccessTokenEndpoint() { + return "https://accounts.google.com/o/oauth2/token"; + } + + @Override + public String getAuthorizationUrl(OAuthConfig config) { + Preconditions.checkValidUrl(config.getCallback(), "Must provide a valid url as callback"); + Validate.isTrue(config.hasScope(), "must contain scope"); + return format(AUTHORIZE_URL, config.getApiKey(), encode(config.getCallback()), encode(config.getScope())); + } + + public Verb getAccessTokenVerb() { + return Verb.POST; + } + + @Override + public AccessTokenExtractor getAccessTokenExtractor() { + return new JsonTokenExtractor(); + } + + @Override + public OAuthService createService(final OAuthConfig config) { + return new OAuth20ServiceImpl(this, config) { + @Override + public Token getAccessToken(Token requestToken, Verifier verifier) { + OAuthRequest request = new OAuthRequest(getAccessTokenVerb(), getAccessTokenEndpoint()); + request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey()); + request.addBodyParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret()); + request.addBodyParameter(OAuthConstants.CODE, verifier.getValue()); + request.addBodyParameter(OAuthConstants.REDIRECT_URI, config.getCallback()); + request.addBodyParameter("grant_type", "authorization_code"); + if (config.hasScope()) { + request.addBodyParameter(OAuthConstants.SCOPE, config.getScope()); + } + Response response = request.send(); + return getAccessTokenExtractor().extract(response.getBody()); + } + }; + } + + static class JsonTokenExtractor implements AccessTokenExtractor { + + @Override + public Token extract(String response) { + try { + return new Token(Json.GSON.fromJson(response, AccessToken.class).getAccessToken(), "", response); + } catch (JsonSyntaxException | NullPointerException e) { + throw new OAuthException("Cannot extract an acces token. Response was: " + response, e); + } + } + + static class AccessToken { + @SerializedName("access_token") + private String accessToken; + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/io/lavagna/web/security/login/oauth/GoogleHandler.java b/src/main/java/io/lavagna/web/security/login/oauth/GoogleHandler.java new file mode 100644 index 000000000..8f0f26559 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/GoogleHandler.java @@ -0,0 +1,73 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import io.lavagna.service.UserRepository; +import io.lavagna.web.security.login.oauth.OAuthResultHandler.OAuthResultHandlerAdapter; + +import org.scribe.builder.ServiceBuilder; + +import com.google.gson.annotations.SerializedName; + +public class GoogleHandler extends OAuthResultHandlerAdapter { + + public GoogleHandler(ServiceBuilder serviceBuilder, OAuthRequestBuilder reqBuilder, String apiKey, + String apiSecret, String callback, UserRepository userRepository, String errorPage) { + super("oauth.google",// + "https://www.googleapis.com/plus/v1/people/me/openIdConnect",// + UserInfo.class, "code",// + userRepository,// + errorPage,// + serviceBuilder.provider(new Google20Api()).apiKey(apiKey).apiSecret(apiSecret).callback(callback) + .scope("openid email").build(), reqBuilder); + } + + static class UserInfo implements RemoteUserProfile { + private String email; + + @SerializedName("email_verified") + private boolean emailVerified; + + @Override + public boolean valid(UserRepository userRepository, String provider) { + return emailVerified && userRepository.userExistsAndEnabled(provider, email); + } + + @Override + public String username() { + return email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(boolean emailVerified) { + this.emailVerified = emailVerified; + } + + } + +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/OAuthResultHandler.java b/src/main/java/io/lavagna/web/security/login/oauth/OAuthResultHandler.java new file mode 100644 index 000000000..48a56bf4b --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/OAuthResultHandler.java @@ -0,0 +1,154 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import io.lavagna.common.Json; +import io.lavagna.service.UserRepository; +import io.lavagna.web.helper.Redirector; +import io.lavagna.web.helper.UserSession; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.scribe.model.OAuthRequest; +import org.scribe.model.Response; +import org.scribe.model.Token; +import org.scribe.model.Verb; +import org.scribe.model.Verifier; +import org.scribe.oauth.OAuthService; +import org.springframework.web.util.UriUtils; + +public interface OAuthResultHandler { + + void handleAuthorizationUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException; + + void handleCallback(HttpServletRequest req, HttpServletResponse resp) throws IOException; + + class OAuthResultHandlerAdapter implements OAuthResultHandler { + + private final String provider; + private final String profileUrl; + private final Class profileClass; + private final String verifierParamName; + + private final UserRepository userRepository; + private final String errorPage; + protected final OAuthService oauthService; + private final OAuthRequestBuilder reqBuilder; + + OAuthResultHandlerAdapter(String provider, String profileUrl, Class profileClass, + String verifierParamName, UserRepository userRepository, String errorPage, OAuthService oauthService, + OAuthRequestBuilder reqBuilder) { + this.provider = provider; + this.profileUrl = profileUrl; + this.profileClass = profileClass; + this.verifierParamName = verifierParamName; + // + this.userRepository = userRepository; + this.errorPage = errorPage; + this.oauthService = oauthService; + this.reqBuilder = reqBuilder; + } + + private String stateForAttribute() { + return "EXPECTED_STATE_FOR_" + provider; + } + + @Override + public void handleAuthorizationUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException { + // scribe does not support out of the box the state parameter, must + // be overridden to be removed + String state = UUID.randomUUID().toString(); + saveStateAndRequestUrlParameter(req, state); + resp.sendRedirect(oauthService.getAuthorizationUrl(null) + "&state=" + state); + } + + protected void saveStateAndRequestUrlParameter(HttpServletRequest req, String state) + throws UnsupportedEncodingException { + req.getSession().setAttribute(stateForAttribute(), state); + req.getSession().setAttribute("rememberMe-" + state, req.getParameter("rememberMe")); + + String reqUrl = req.getParameter("reqUrl"); + if (reqUrl != null) { + req.getSession().setAttribute("reqUrl-" + state, UriUtils.decode(reqUrl, "UTF-8")); + } + } + + // only for services that support the state parameter, must be + // overridden to be ignored + protected boolean validateStateParam(HttpServletRequest req) { + String stateParam = req.getParameter("state"); + String expectedState = (String) req.getSession().getAttribute(stateForAttribute()); + req.getSession().removeAttribute(stateForAttribute()); + return expectedState != null && expectedState.equals(stateParam); + } + + @Override + public void handleCallback(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String state = (String) req.getSession().getAttribute(stateForAttribute()); + String reqUrl = (String) req.getSession().getAttribute("reqUrl-" + state); + req.setAttribute("rememberMe", req.getSession().getAttribute("rememberMe-" + state)); + req.getSession().removeAttribute("reqUrl-" + state); + req.getSession().removeAttribute("rememberMe-" + state); + + if (!validateStateParam(req)) { + Redirector.sendRedirect(req, resp, errorPage); + return; + } + + // verify token + Verifier verifier = new Verifier(req.getParameter(verifierParamName)); + Token accessToken = oauthService.getAccessToken(reqToken(req), verifier); + + // fetch user profile + OAuthRequest oauthRequest = reqBuilder.req(Verb.GET, profileUrl); + oauthService.signRequest(accessToken, oauthRequest); + Response oauthResponse = oauthRequest.send(); + RemoteUserProfile profile = Json.GSON.fromJson(oauthResponse.getBody(), profileClass); + + if (profile.valid(userRepository, provider)) { + String url = Redirector.cleanupRequestedUrl(reqUrl); + UserSession.setUser(userRepository.findUserByName(provider, profile.username()), req, resp, + userRepository); + Redirector.sendRedirect(req, resp, url); + } else { + Redirector.sendRedirect(req, resp, errorPage); + } + } + + protected Token reqToken(HttpServletRequest req) { + return null; + } + } + + public static class OAuthRequestBuilder { + + public OAuthRequest req(Verb verb, String url) { + return new OAuthRequest(verb, url); + } + } + + interface RemoteUserProfile { + boolean valid(UserRepository userRepository, String provider); + + String username(); + } +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/TwitterHandler.java b/src/main/java/io/lavagna/web/security/login/oauth/TwitterHandler.java new file mode 100644 index 000000000..6f6066cb0 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/TwitterHandler.java @@ -0,0 +1,52 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import io.lavagna.service.UserRepository; + +import org.scribe.builder.ServiceBuilder; +import org.scribe.builder.api.TwitterApi; + +import com.google.gson.annotations.SerializedName; + +public class TwitterHandler extends AbstractOAuth1Handler { + + public TwitterHandler(ServiceBuilder serviceBuilder, OAuthRequestBuilder reqBuilder, String apiKey, + String apiSecret, String callback, UserRepository userRepository, String errorPage) { + super("oauth.twitter", "https://api.twitter.com/1.1/account/verify_credentials.json", UserInfo.class, + "oauth_verifier", userRepository, errorPage, serviceBuilder.provider(TwitterApi.class) + .apiKey(apiKey).apiSecret(apiSecret).callback(callback).build(), reqBuilder); + } + + private static class UserInfo implements RemoteUserProfile { + + @SerializedName("screen_name") + private String screenName; + + @Override + public boolean valid(UserRepository userRepository, String provider) { + return userRepository.userExistsAndEnabled(provider, username()); + } + + @Override + public String username() { + return screenName; + } + + } + +} diff --git a/src/main/java/io/lavagna/web/security/login/oauth/Utils.java b/src/main/java/io/lavagna/web/security/login/oauth/Utils.java new file mode 100644 index 000000000..905394616 --- /dev/null +++ b/src/main/java/io/lavagna/web/security/login/oauth/Utils.java @@ -0,0 +1,34 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.security.login.oauth; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +final class Utils { + + private Utils() { + } + + static String encode(String string) { + try { + return URLEncoder.encode(string, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/io/lavagna/web/support/ResourceController.java b/src/main/java/io/lavagna/web/support/ResourceController.java new file mode 100644 index 000000000..819075674 --- /dev/null +++ b/src/main/java/io/lavagna/web/support/ResourceController.java @@ -0,0 +1,344 @@ +/** + * This file is part of lavagna. + * + * lavagna 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. + * + * lavagna 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 lavagna. If not, see . + */ +package io.lavagna.web.support; + +import static org.apache.commons.lang3.ArrayUtils.contains; +import io.lavagna.common.Json; +import io.lavagna.model.Permission; +import io.lavagna.web.helper.ExpectPermission; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.stereotype.Controller; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.samskivert.mustache.Mustache; + +@Controller +public class ResourceController { + + private static final String PROJ_SHORT_NAME = "{projectShortName:[A-Z0-9_]+}"; + private static final String BOARD_SHORT_NAME = "{shortName:[A-Z0-9_]+}"; + private static final String CARD_SEQ = "{cardId:[0-9]+}"; + private final Environment env; + // we don't care if the values are set more than one time + private final AtomicReference indexCache = new AtomicReference<>(); + private final AtomicReference jsCache = new AtomicReference<>(); + private final AtomicReference cssCache = new AtomicReference<>(); + + @Autowired + public ResourceController(Environment env) { + this.env = env; + } + + private static List prepareTemplates(ServletContext context, String initialPath) throws IOException { + List r = new ArrayList<>(); + BeforeAfter ba = new AngularTemplate(); + for (String file : allFilesWithExtension(context, initialPath, ".html")) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + output(file, context, os, ba); + r.add(os.toString(StandardCharsets.UTF_8.displayName())); + } + return r; + } + + private static Set allFilesWithExtension(ServletContext context, String initialPath, String extension) { + Set res = new TreeSet<>(); + extractFilesWithExtensionRec(context, initialPath, extension, res); + return res; + } + + private static void extractFilesWithExtensionRec(ServletContext context, String initialPath, String extension, + Set res) { + for (String s : context.getResourcePaths(initialPath)) { + if (s.endsWith("/")) { + extractFilesWithExtensionRec(context, s, extension, res); + } else if (s.endsWith(extension)) { + res.add(s); + } + } + } + + private static void concatenateOutput(String directory, String fileExtension, ServletContext context, + OutputStream os, BeforeAfter ba) throws IOException { + for (String res : new TreeSet<>(context.getResourcePaths(directory))) { + if (res.endsWith(fileExtension)) { + output(res, context, os, ba); + } + } + } + + private static void output(String file, ServletContext context, OutputStream os, BeforeAfter ba) throws IOException { + ba.before(file, context, os); + StreamUtils.copy(context.getResourceAsStream(file), os); + ba.after(file, context, os); + os.flush(); + } + + @ExpectPermission(Permission.ADMINISTRATION) + @RequestMapping(value = { "admin/", "admin/configure-login/", "admin/manage-users/", "admin/role/", + "admin/export-import/", "admin/endpoint-info/", "admin/parameters/", + "admin/manage-anonymous-users-access/", "admin/manage-smtp-configuration/" }, method = RequestMethod.GET) + public void handleIndexForAdmin(HttpServletRequest request, HttpServletResponse response) throws IOException { + handleIndex(request, response); + } + + @ExpectPermission(Permission.PROJECT_ADMINISTRATION) + @RequestMapping(value = { PROJ_SHORT_NAME + "/manage/",// + PROJ_SHORT_NAME + "/manage/project/",// + PROJ_SHORT_NAME + "/manage/boards/",// + PROJ_SHORT_NAME + "/manage/roles/",// + PROJ_SHORT_NAME + "/manage/labels/",// + PROJ_SHORT_NAME + "/manage/import/",// + PROJ_SHORT_NAME + "/manage/milestones/",// + PROJ_SHORT_NAME + "/manage/anonymous-users-access/",// + PROJ_SHORT_NAME + "/manage/columns-status/" }, method = RequestMethod.GET) + public void handleIndexForProjectAdmin(HttpServletRequest request, HttpServletResponse response) throws IOException { + handleIndex(request, response); + } + + @ExpectPermission(Permission.UPDATE_PROFILE) + @RequestMapping(value = "me", method = RequestMethod.GET) + public void handleIndexForMe(HttpServletRequest request, HttpServletResponse response) throws IOException { + handleIndex(request, response); + } + + @RequestMapping(value = { "/",// + "user/{provider}/{username}", "user/{provider}/{username}/projects/", "user/{provider}/{username}/activity/",// + "search/",// + "search/" + PROJ_SHORT_NAME + "/" + BOARD_SHORT_NAME + "-" + CARD_SEQ,// + PROJ_SHORT_NAME + "/",// + PROJ_SHORT_NAME + "/search/",// + PROJ_SHORT_NAME + "/search/" + BOARD_SHORT_NAME + "-" + CARD_SEQ,// / + PROJ_SHORT_NAME + "/" + BOARD_SHORT_NAME,// + PROJ_SHORT_NAME + "/statistics/",// + PROJ_SHORT_NAME + "/milestones/",// + PROJ_SHORT_NAME + "/milestones/" + BOARD_SHORT_NAME + "-" + CARD_SEQ,// + PROJ_SHORT_NAME + "/" + BOARD_SHORT_NAME + "-" + CARD_SEQ }, method = RequestMethod.GET) + public void handleIndex(HttpServletRequest request, HttpServletResponse response) throws IOException { + + ServletContext context = request.getServletContext(); + + if (contains(env.getActiveProfiles(), "dev") || indexCache.get() == null) { + + ByteArrayOutputStream index = new ByteArrayOutputStream(); + output("/index.html", context, index, new BeforeAfter()); + + Map data = new HashMap<>(); + data.put("contextPath", request.getServletContext().getContextPath() + "/"); + data.put("inlineTemplates", prepareTemplates(context, "/partials/")); + + indexCache.set(Mustache.compiler().escapeHTML(false) + .compile(index.toString(StandardCharsets.UTF_8.displayName())).execute(data) + .getBytes(StandardCharsets.UTF_8)); + } + + try (OutputStream os = response.getOutputStream()) { + response.setContentType("text/html; charset=UTF-8"); + StreamUtils.copy(indexCache.get(), os); + } + } + + /** + * Dynamically load and concatenate the js present in the configured directories + * + * @param request + * @param response + * @throws IOException + */ + @RequestMapping(value = "/resource/app.js", method = RequestMethod.GET) + public void handleJs(HttpServletRequest request, HttpServletResponse response) throws IOException { + + if (contains(env.getActiveProfiles(), "dev") || jsCache.get() == null) { + ServletContext context = request.getServletContext(); + response.setContentType("text/javascript"); + BeforeAfter ba = new JS(); + ByteArrayOutputStream allJs = new ByteArrayOutputStream(); + + // + for (String res : Arrays.asList("/js/d3.v3.min.js", "/js/cal-heatmap.min.js",// + "/js/jquery.min.js", "/js/jquery-ui.min.js",// + "/js/jquery.ui.touch-punch.min.js",// + "/js/highlight.pack.js",// + "/js/marked.js",// + "/js/sockjs.min.js", "/js/stomp.min.js",// + "/js/angular-file-upload-html5-shim.js",// + "/js/angular.min.js", "/js/angular-sanitize.min.js",// + "/js/angular-ui-router.min.js",// + "/js/angular-uuid2.min.js",// + "/js/angular-file-upload.min.js",// + "/js/bindonce.min.js",// + "/js/angular-translate.min.js",// + "/js/sortable.js", // + "/js/spectrum.js", // + "/js/codemirror-compressed.js",// + "/js/peg-0.8.0.min.js",// + "/js/moment.min.js",// + "/js/Chart.min.js",// + "/js/ui-bootstrap-tpls-0.11.0.min.js",// + "/js/df-tab-menu.min.js",// + "/js/df-autocomplete.js")) { + output(res, context, allJs, ba); + } + // + + // + addMessages(context, allJs, ba); + // + + output("/app/app.js", context, allJs, ba); + concatenateOutput("/app/controllers/", ".js", context, allJs, ba); + concatenateOutput("/app/controllers/admin/", ".js", context, allJs, ba); + concatenateOutput("/app/controllers/project/", ".js", context, allJs, ba); + concatenateOutput("/app/directives/", ".js", context, allJs, ba); + concatenateOutput("/app/filters/", ".js", context, allJs, ba); + concatenateOutput("/app/services/", ".js", context, allJs, ba); + + jsCache.set(allJs.toByteArray()); + + } + + try (OutputStream os = response.getOutputStream()) { + response.setContentType("text/javascript"); + StreamUtils.copy(jsCache.get(), os); + } + } + + private void addMessages(ServletContext context, OutputStream os, BeforeAfter ba) throws IOException { + ba.before("i18n", context, os); + // + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath:io/lavagna/i18n/messages_*.properties"); + // + os.write(("window.io_lavagna=window.io_lavagna||{};window.io_lavagna.i18n=" + Json.GSON + .toJson(fromResources(resources))).getBytes(StandardCharsets.UTF_8)); + ba.after("i18n", context, os); + } + + private static Map> fromResources(Resource[] resources) throws IOException { + + Pattern extractLanguage = Pattern.compile("^messages_(.*)\\.properties$"); + + Map> langs = new HashMap<>(); + + for (Resource res : resources) { + Matcher matcher = extractLanguage.matcher(res.getFilename()); + matcher.find(); + String lang = matcher.group(1); + Properties p = new Properties(); + p.load(res.getInputStream()); + langs.put(lang, new HashMap(p)); + } + return langs; + } + + @RequestMapping(value = "/css/all.css", method = RequestMethod.GET) + public void handleCss(HttpServletRequest request, HttpServletResponse response) throws IOException { + + if (contains(env.getActiveProfiles(), "dev") || cssCache.get() == null) { + ByteArrayOutputStream cssOs = new ByteArrayOutputStream(); + ServletContext context = request.getServletContext(); + BeforeAfter ba = new BeforeAfter(); + for (String res : Arrays.asList("/css/bootstrap.css",// + "/css/highlight-default.css",// + "/css/jquery-ui.css",// + "/css/spectrum.css",// + "/css/codemirror.css",// + "/css/font-awesome.css",// + "/css/df-tab-menu.css",// + "/css/df-autocomplete.css",// + "/css/lvg-general.css",// + "/css/lvg-navigation.css",// + "/css/lvg-project.css",// + "/css/lvg-board.css",// + "/css/lvg-card.css",// + "/css/lvg-admin.css",// + "/css/lvg-user.css",// + "/css/lvg-search.css", + "/css/lvg-login.css")) { + output(res, context, cssOs, ba); + } + + cssCache.set(cssOs.toByteArray()); + } + + try (OutputStream os = response.getOutputStream()) { + response.setContentType("text/css"); + StreamUtils.copy(cssCache.get(), os); + } + } + + private static class BeforeAfter { + void before(String file, ServletContext context, OutputStream os) throws IOException { + } + + void after(String file, ServletContext context, OutputStream os) throws IOException { + } + } + + private static class JS extends BeforeAfter { + + @Override + public void before(String file, ServletContext context, OutputStream os) throws IOException { + os.write((";\n\n /* begin " + file + " */ \n\n").getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void after(String file, ServletContext context, OutputStream os) throws IOException { + os.write((";\n\n /* end " + file + " */ \n\n").getBytes(StandardCharsets.UTF_8)); + } + } + + private static class AngularTemplate extends BeforeAfter { + @Override + void before(String file, ServletContext context, OutputStream os) throws IOException { + os.write(("".getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/src/main/resources/io/lavagna/db/HSQLDB/V1__INITIAL_VERSION.sql b/src/main/resources/io/lavagna/db/HSQLDB/V1__INITIAL_VERSION.sql new file mode 100644 index 000000000..3c107260a --- /dev/null +++ b/src/main/resources/io/lavagna/db/HSQLDB/V1__INITIAL_VERSION.sql @@ -0,0 +1,641 @@ +-- +-- This file is part of lavagna. +-- +-- lavagna 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. +-- +-- lavagna 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 lavagna. If not, see . +-- + +-- CONFIGURATION TABLE +CREATE TABLE LA_CONF ( + CONF_KEY VARCHAR(64) PRIMARY KEY NOT NULL, + CONF_VALUE CLOB NOT NULL +); + +-- USER +CREATE TABLE LA_USER ( + USER_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + USER_PROVIDER VARCHAR(16) NOT NULL, + USER_NAME VARCHAR(128) NOT NULL, + USER_EMAIL VARCHAR(256), + USER_ENABLED BOOLEAN, + USER_DISPLAY_NAME VARCHAR(256), + USER_EMAIL_NOTIFICATION BOOLEAN DEFAULT TRUE NOT NULL, + USER_LAST_EMAIL_SENT TIMESTAMP DEFAULT NULL, + USER_LAST_CHECKPOINT_COUNT INTEGER DEFAULT 0 NOT NULL, + USER_LAST_CHECKED TIMESTAMP DEFAULT NULL, + USER_MEMBER_SINCE TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); +-- CONSTRAINTS +ALTER TABLE LA_USER ADD CONSTRAINT "UNIQUE_LA_USER_USER_NAME" UNIQUE(USER_PROVIDER, USER_NAME); + +-- USER REMEMBER +CREATE TABLE LA_USER_REMEMBER ( + USER_REMEMBER_HASHED_TOKEN CHAR(64) NOT NULL, + USER_REMEMBER_ID_FK INTEGER NOT NULL, + USER_REMEMBER_LAST_USE TIMESTAMP NOT NULL, + PRIMARY KEY(USER_REMEMBER_ID_FK, USER_REMEMBER_HASHED_TOKEN) +); +-- CONSTRAINTS +ALTER TABLE LA_USER_REMEMBER ADD FOREIGN KEY(USER_REMEMBER_ID_FK) REFERENCES LA_USER(USER_ID); + +-- PROJECT +CREATE TABLE LA_PROJECT ( + PROJECT_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + PROJECT_NAME VARCHAR(128) NOT NULL, + PROJECT_SHORT_NAME VARCHAR(8) NOT NULL, + PROJECT_ARCHIVED BOOLEAN DEFAULT FALSE NOT NULL, + PROJECT_DESCRIPTION CLOB +); +ALTER TABLE LA_PROJECT ADD CONSTRAINT "UNIQUE_LA_PROJECT_SHORT_NAME" UNIQUE(PROJECT_SHORT_NAME); + +-- BOARDS +CREATE TABLE LA_BOARD ( + BOARD_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + BOARD_PROJECT_ID_FK INTEGER NOT NULL, + BOARD_NAME VARCHAR(128) NOT NULL, + BOARD_SHORT_NAME VARCHAR(8) NOT NULL, + BOARD_ARCHIVED BOOLEAN DEFAULT FALSE NOT NULL, + BOARD_DESCRIPTION CLOB +); +-- CONSTRAINTS +ALTER TABLE LA_BOARD ADD CONSTRAINT "UNIQUE_LA_BOARD_SHORT_NAME" UNIQUE(BOARD_SHORT_NAME); +ALTER TABLE LA_BOARD ADD FOREIGN KEY(BOARD_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +-- INDEXES +CREATE INDEX "BOARD_PROJECT_ID_FK_IDX" ON LA_BOARD(BOARD_PROJECT_ID_FK); + +-- BOARD COUNTERS +CREATE TABLE LA_BOARD_COUNTER ( + BOARD_COUNTER_ID_FK INTEGER PRIMARY KEY NOT NULL, + BOARD_COUNTER_CARD_SEQUENCE INTEGER NOT NULL +); +ALTER TABLE LA_BOARD_COUNTER ADD FOREIGN KEY(BOARD_COUNTER_ID_FK) REFERENCES LA_BOARD(BOARD_ID); + +-- BOARD DEFINITION, FOR STATISTICS AND MICROMANAGEMENT +CREATE TABLE LA_BOARD_COLUMN_DEFINITION ( + BOARD_COLUMN_DEFINITION_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + BOARD_COLUMN_DEFINITION_PROJECT_ID_FK INTEGER NOT NULL, + BOARD_COLUMN_DEFINITION_VALUE VARCHAR(24) NOT NULL, + BOARD_COLUMN_DEFINITION_COLOR INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD FOREIGN KEY(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD CONSTRAINT "BOARD_COLUMN_DEFINITION_VALUE" CHECK(BOARD_COLUMN_DEFINITION_VALUE IN ('OPEN', 'CLOSED', 'BACKLOG', 'DEFERRED')); +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD CONSTRAINT "BOARD_COLUMN_DEFINITION_COLOR_UNIQUE" UNIQUE(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_COLOR); + + +-- BOARD COLUMN, FOR CATEGORIZING A CARD +CREATE TABLE LA_BOARD_COLUMN ( + BOARD_COLUMN_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + BOARD_COLUMN_NAME VARCHAR(128) NOT NULL, + BOARD_COLUMN_ORDER INTEGER NOT NULL, + BOARD_COLUMN_LOCATION VARCHAR(16) NOT NULL, + BOARD_COLUMN_BOARD_ID_FK INTEGER NOT NULL, + BOARD_COLUMN_DEFINITION_ID_FK INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_BOARD_COLUMN ADD FOREIGN KEY(BOARD_COLUMN_BOARD_ID_FK) REFERENCES LA_BOARD(BOARD_ID); +ALTER TABLE LA_BOARD_COLUMN ADD FOREIGN KEY(BOARD_COLUMN_DEFINITION_ID_FK) REFERENCES LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_ID); +ALTER TABLE LA_BOARD_COLUMN ADD CONSTRAINT "BOARD_COLUMN_LOCATION_VALUE" CHECK(BOARD_COLUMN_LOCATION IN ('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH')); +ALTER TABLE LA_BOARD_COLUMN ADD CONSTRAINT "BOARD_COLUMN_LOCATION_AND_NAME" CHECK( +CASE + WHEN BOARD_COLUMN_LOCATION = 'BOARD' AND BOARD_COLUMN_NAME IN ('BACKLOG', 'ARCHIVE', 'TRASH') THEN FALSE + ELSE TRUE +END); +-- INDEXES +CREATE INDEX "BOARD_COLUMN_BOARD_ID_FK_IDX" ON LA_BOARD_COLUMN(BOARD_COLUMN_BOARD_ID_FK); +CREATE INDEX "BOARD_COLUMN_DEFINITION_ID_FK_IDX" ON LA_BOARD_COLUMN(BOARD_COLUMN_DEFINITION_ID_FK); + +-- BOARD STATISTICS +CREATE TABLE LA_BOARD_STATISTICS ( + BOARD_STATISTICS_TIME TIMESTAMP NOT NULL, + BOARD_STATISTICS_BOARD_ID_FK INTEGER NOT NULL, + BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK INTEGER NOT NULL, + BOARD_STATISTICS_LOCATION VARCHAR(16) NOT NULL, + BOARD_STATISTICS_COUNT INTEGER NOT NULL +); +ALTER TABLE LA_BOARD_STATISTICS ADD FOREIGN KEY(BOARD_STATISTICS_BOARD_ID_FK) REFERENCES LA_BOARD(BOARD_ID); +ALTER TABLE LA_BOARD_STATISTICS ADD FOREIGN KEY(BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK) REFERENCES LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_ID); +ALTER TABLE LA_BOARD_STATISTICS ADD CONSTRAINT "BOARD_STATISTICS_LOCATION_VALUE" CHECK(BOARD_STATISTICS_LOCATION IN ('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH')); +-- INDEXES +CREATE INDEX "BOARD_STATISTICS_BOARD_ID_FK_IDX" ON LA_BOARD_STATISTICS(BOARD_STATISTICS_BOARD_ID_FK); + +-- LATEST BOARD STATISTICS BY DAY +CREATE VIEW LA_BOARD_STATISTICS_DAYS AS (SELECT MAX(BOARD_STATISTICS_TIME) AS DAY FROM LA_BOARD_STATISTICS GROUP BY CAST(BOARD_STATISTICS_TIME AS DATE)); + +-- CARD +CREATE TABLE LA_CARD ( + CARD_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + CARD_NAME VARCHAR(255) NOT NULL, + CARD_BOARD_COLUMN_ID_FK INTEGER NOT NULL, + CARD_ORDER INTEGER NOT NULL, + CARD_SEQ_NUMBER INTEGER NOT NULL, + CARD_USER_ID_FK INTEGER NOT NULL, + CARD_LAST_UPDATED TIMESTAMP NOT NULL, + CARD_LAST_UPDATED_USER_ID_FK INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_BOARD_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_LAST_UPDATED_USER_ID_FK) REFERENCES LA_USER(USER_ID); +-- INDEXES +CREATE INDEX "CARD_BOARD_COLUMN_ID_FK_IDX" ON LA_CARD(CARD_BOARD_COLUMN_ID_FK); +CREATE INDEX "CARD_USER_ID_FK_IDX" ON LA_CARD(CARD_USER_ID_FK); +CREATE INDEX "CARD_LAST_UPDATED_USER_ID_FK_IDX" ON LA_CARD(CARD_LAST_UPDATED_USER_ID_FK); + + +CREATE TABLE LA_CARD_DATA ( + CARD_DATA_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + CARD_DATA_CARD_ID_FK INTEGER NOT NULL, + CARD_DATA_REFERENCE_ID INTEGER, + CARD_DATA_DELETED BOOLEAN DEFAULT FALSE NOT NULL, + CARD_DATA_TYPE VARCHAR(128) NOT NULL, + CARD_DATA_CONTENT CLOB NOT NULL, + CARD_DATA_ORDER INTEGER NOT NULL +); + + +ALTER TABLE LA_CARD_DATA ADD FOREIGN KEY(CARD_DATA_CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_DATA ADD FOREIGN KEY(CARD_DATA_REFERENCE_ID) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_CARD_DATA ADD CONSTRAINT "CARD_DATA_TYPE_VALUE" CHECK(CARD_DATA_TYPE IN ('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'FILE', 'COMMENT_HISTORY', 'DESCRIPTION', 'DESCRIPTION_HISTORY')); +ALTER TABLE LA_CARD_DATA ADD CONSTRAINT "CARD_DATA_ENSURE_REFERENCE_ID" CHECK( +CASE + WHEN CARD_DATA_TYPE IN ('COMMENT','ACTION_LIST', 'DESCRIPTION') THEN CARD_DATA_REFERENCE_ID IS NULL + WHEN CARD_DATA_TYPE IN ('ACTION_CHECKED', 'ACTION_UNCHECKED', 'COMMENT_HISTORY', 'DESCRIPTION_HISTORY') THEN CARD_DATA_REFERENCE_ID IS NOT NULL + ELSE TRUE +END); +-- INDEXES +CREATE INDEX "CARD_DATA_CARD_ID_FK_IDX" ON LA_CARD_DATA(CARD_DATA_CARD_ID_FK); +CREATE INDEX "CARD_DATA_REFERENCE_ID_IDX" ON LA_CARD_DATA(CARD_DATA_REFERENCE_ID); + +-- DATA UPLOAD +CREATE TABLE LA_CARD_DATA_UPLOAD_CONTENT ( + DIGEST CHAR(64) NOT NULL, + SIZE INTEGER NOT NULL, + CONTENT BLOB NOT NULL, + CONTENT_TYPE VARCHAR(255) NOT NULL +); +ALTER TABLE LA_CARD_DATA_UPLOAD_CONTENT ADD CONSTRAINT "UNIQUE_LA_CARD_UPLOAD_CONTENT" UNIQUE(DIGEST); + +CREATE TABLE LA_CARD_DATA_UPLOAD ( + CARD_DATA_ID_FK INTEGER NOT NULL, + CARD_DATA_UPLOAD_CONTENT_DIGEST_FK CHAR(64) NOT NULL, + ORIGINAL_NAME VARCHAR(255) NOT NULL, + DISPLAYED_NAME VARCHAR(255) NOT NULL +); +ALTER TABLE LA_CARD_DATA_UPLOAD ADD FOREIGN KEY(CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_CARD_DATA_UPLOAD ADD FOREIGN KEY(CARD_DATA_UPLOAD_CONTENT_DIGEST_FK) REFERENCES LA_CARD_DATA_UPLOAD_CONTENT(DIGEST); +-- INDEXES +CREATE INDEX "CARD_DATA_UPLOAD_CONTENT_DIGEST_FK_IDX" ON LA_CARD_DATA_UPLOAD(CARD_DATA_UPLOAD_CONTENT_DIGEST_FK); + +-- CARD LABEL +CREATE TABLE LA_CARD_LABEL ( + CARD_LABEL_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + CARD_LABEL_PROJECT_ID_FK INTEGER NOT NULL, + CARD_LABEL_UNIQUE BOOLEAN DEFAULT FALSE NOT NULL, + CARD_LABEL_TYPE VARCHAR(16) NOT NULL, + CARD_LABEL_DOMAIN VARCHAR(16) NOT NULL, + CARD_LABEL_NAME VARCHAR(32) NOT NULL, + CARD_LABEL_COLOR INTEGER +); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT "CARD_LABEL_TYPE_VALUE" CHECK(CARD_LABEL_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST')); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT "CARD_LABEL_DOMAIN_VALUE" CHECK(CARD_LABEL_DOMAIN IN ('SYSTEM', 'USER')); +ALTER TABLE LA_CARD_LABEL ADD FOREIGN KEY(CARD_LABEL_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_PROJECT_ID_FK_NAME" UNIQUE(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_NAME); +-- INDEXES +CREATE INDEX "CARD_LABEL_PROJECT_ID_FK_IDX" ON LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK); +-- + +-- junction table label <-> list values +CREATE TABLE LA_CARD_LABEL_LIST_VALUE ( + CARD_LABEL_LIST_VALUE_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + CARD_LABEL_ID_FK INTEGER NOT NULL, + CARD_LABEL_LIST_VALUE_ORDER INTEGER NOT NULL, + CARD_LABEL_LIST_VALUE VARCHAR(255) +); +ALTER TABLE LA_CARD_LABEL_LIST_VALUE ADD FOREIGN KEY(CARD_LABEL_ID_FK) REFERENCES LA_CARD_LABEL(CARD_LABEL_ID); +-- INDEXES +CREATE INDEX "LA_CARD_LABEL_LIST_VALUE_CARD_LABEL_ID_FK_IDX" ON LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK); +-- + + +-- junction table label <-> card and card value +CREATE TABLE LA_CARD_LABEL_VALUE ( + CARD_LABEL_VALUE_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + CARD_LABEL_VALUE_USE_UNIQUE_INDEX BOOLEAN DEFAULT NULL, + CARD_LABEL_VALUE_DELETED BOOLEAN DEFAULT FALSE NOT NULL, + CARD_ID_FK INTEGER NOT NULL, + CARD_LABEL_ID_FK INTEGER NOT NULL, + CARD_LABEL_VALUE_TYPE VARCHAR(16) NOT NULL, + CARD_LABEL_VALUE_STRING VARCHAR(32), + CARD_LABEL_VALUE_TIMESTAMP TIMESTAMP, + CARD_LABEL_VALUE_INT INTEGER, + CARD_LABEL_VALUE_CARD_FK INTEGER, + CARD_LABEL_VALUE_USER_FK INTEGER, + CARD_LABEL_VALUE_LIST_VALUE_FK INTEGER +); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_ID_FK) REFERENCES LA_CARD_LABEL(CARD_LABEL_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_VALUE_CARD_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_VALUE_USER_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "CARD_LABEL_VALUE_TYPE_VALUE" CHECK(CARD_LABEL_VALUE_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST')); +-- +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_STRING" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_CARD_FK" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_CARD_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_USER_FK" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_USER_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_LIST_VALUE_FK" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_USE_UNIQUE_INDEX" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_USE_UNIQUE_INDEX); +-- +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "CARD_LABEL_VALUE_ENSURE_TYPE" CHECK( +CASE + WHEN CARD_LABEL_VALUE_TYPE = 'NULL' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'STRING' THEN + CARD_LABEL_VALUE_STRING IS NOT NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'TIMESTAMP' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NOT NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'INT' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NOT NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'CARD' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NOT NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'USER' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NOT NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'LIST' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NOT NULL +END); +-- INDEXES +CREATE INDEX "LA_CARD_LABEL_VALUE_CARD_ID_FK_IDX" ON LA_CARD_LABEL_VALUE(CARD_ID_FK); +CREATE INDEX "LA_CARD_LABEL_VALUE_CARD_LABEL_ID_FK_IDX" ON LA_CARD_LABEL_VALUE(CARD_LABEL_ID_FK); + +-- EVENT + +CREATE TABLE LA_EVENT ( + EVENT_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + EVENT_CARD_ID_FK INTEGER NOT NULL, + EVENT_USER_ID_FK INTEGER NOT NULL, + EVENT_TYPE VARCHAR(32) NOT NULL, + EVENT_TIME TIMESTAMP NOT NULL, + EVENT_CARD_DATA_ID_FK INTEGER, + EVENT_PREV_CARD_DATA_ID_FK INTEGER, + EVENT_NEW_CARD_DATA_ID_FK INTEGER, + EVENT_COLUMN_ID_FK INTEGER, + EVENT_PREV_COLUMN_ID_FK INTEGER, + EVENT_LABEL_NAME VARCHAR(32), + EVENT_LABEL_TYPE VARCHAR(16) DEFAULT 'NULL' NOT NULL, + EVENT_VALUE_INT INTEGER, + EVENT_VALUE_STRING VARCHAR(255) NULL, + EVENT_VALUE_TIMESTAMP TIMESTAMP NULL, + EVENT_VALUE_CARD_FK INTEGER, + EVENT_VALUE_USER_FK INTEGER +); +-- CONSTRAINTS; +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_EVENT ADD CONSTRAINT "EVENT_TYPE_VALUE" CHECK(EVENT_TYPE IN ('LABEL_CREATE', 'LABEL_DELETE', 'CARD_MOVE', 'CARD_CREATE', 'CARD_ARCHIVE', 'CARD_BACKLOG', 'CARD_TRASH', 'CARD_UPDATE', 'ACTION_LIST_CREATE', 'ACTION_LIST_DELETE', 'ACTION_ITEM_CREATE', 'ACTION_ITEM_DELETE', 'ACTION_ITEM_MOVE', 'ACTION_ITEM_CHECK', 'ACTION_ITEM_UNCHECK', 'COMMENT_CREATE', 'COMMENT_UPDATE', 'COMMENT_DELETE', 'FILE_UPLOAD', 'FILE_DELETE', 'DESCRIPTION_CREATE', 'DESCRIPTION_UPDATE')); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_PREV_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_PREV_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_NEW_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD CONSTRAINT "EVENT_LABEL_TYPE_VALUE" CHECK(EVENT_LABEL_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER')); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_VALUE_CARD_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_VALUE_USER_FK) REFERENCES LA_USER(USER_ID); + + +--ROLE+PERMISSION +CREATE TABLE LA_ROLE ( + ROLE_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + ROLE_NAME VARCHAR(32) NOT NULL, + ROLE_REMOVABLE BOOLEAN NOT NULL, + ROLE_HIDDEN BOOLEAN DEFAULT FALSE NOT NULL, + ROLE_READONLY BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE LA_ROLE ADD CONSTRAINT "UNIQUE_LA_ROLE_ROLE_NAME" UNIQUE(ROLE_NAME); + + +-- table role <-> permission +CREATE TABLE LA_ROLE_PERMISSION ( + ROLE_ID_FK INTEGER NOT NULL, + PERMISSION VARCHAR(64) NOT NULL +); +ALTER TABLE LA_ROLE_PERMISSION ADD FOREIGN KEY(ROLE_ID_FK) REFERENCES LA_ROLE(ROLE_ID); +ALTER TABLE LA_ROLE_PERMISSION ADD CONSTRAINT "UNIQUE_LA_ROLE_PERMISSION" UNIQUE(ROLE_ID_FK, PERMISSION); + +-- junction table user <-> role for base role +CREATE TABLE LA_USER_ROLE ( + USER_ID_FK INTEGER NOT NULL, + ROLE_ID_FK INTEGER NOT NULL +); +ALTER TABLE LA_USER_ROLE ADD FOREIGN KEY(USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_USER_ROLE ADD FOREIGN KEY(ROLE_ID_FK) REFERENCES LA_ROLE(ROLE_ID); +ALTER TABLE LA_USER_ROLE ADD CONSTRAINT "UNIQUE_LA_USER_ROLE" UNIQUE(USER_ID_FK, ROLE_ID_FK); + + +-- PROJECTS and PROJECT ROLE HANDLING + +CREATE TABLE LA_PROJECT_ROLE ( + PROJECT_ROLE_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL, + PROJECT_ROLE_NAME VARCHAR(32) NOT NULL, + PROJECT_ID_FK INTEGER NOT NULL, + PROJECT_ROLE_REMOVABLE BOOLEAN DEFAULT TRUE NOT NULL, + PROJECT_ROLE_HIDDEN BOOLEAN DEFAULT FALSE NOT NULL, + PROJECT_ROLE_READONLY BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE LA_PROJECT_ROLE ADD CONSTRAINT "UNIQUE_LA_PROJECT_ROLE_NAME" UNIQUE(PROJECT_ID_FK, PROJECT_ROLE_NAME); +ALTER TABLE LA_PROJECT_ROLE ADD FOREIGN KEY(PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); + +CREATE TABLE LA_PROJECT_ROLE_PERMISSION ( + PROJECT_ROLE_ID_FK INTEGER NOT NULL, + PERMISSION VARCHAR(64) NOT NULL +); +ALTER TABLE LA_PROJECT_ROLE_PERMISSION ADD FOREIGN KEY(PROJECT_ROLE_ID_FK) REFERENCES LA_PROJECT_ROLE(PROJECT_ROLE_ID); +ALTER TABLE LA_PROJECT_ROLE_PERMISSION ADD CONSTRAINT "UNIQUE_LA_PROJECT_ROLE_PERMISSION" UNIQUE(PROJECT_ROLE_ID_FK, PERMISSION); + + +-- junction table project <-> user <-> role +CREATE TABLE LA_PROJECT_USER_ROLE ( + PROJECT_ID_FK INTEGER NOT NULL, + USER_ID_FK INTEGER NOT NULL, + PROJECT_ROLE_ID_FK INTEGER NOT NULL +); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(PROJECT_ROLE_ID_FK) REFERENCES LA_PROJECT_ROLE(PROJECT_ROLE_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD CONSTRAINT "UNIQUE_LA_PROJECT_USER_ROLE" UNIQUE(PROJECT_ID_FK, USER_ID_FK, PROJECT_ROLE_ID_FK); + +-- + +-- TRIGGERS FOR TIME STATS + +CREATE TRIGGER TRIG_CARD_TIME_STATS_INS AFTER INSERT ON LA_EVENT + REFERENCING NEW ROW AS newrow + FOR EACH ROW + UPDATE LA_CARD SET CARD_LAST_UPDATED = newrow.EVENT_TIME, CARD_LAST_UPDATED_USER_ID_FK = newrow.EVENT_USER_ID_FK WHERE CARD_ID = newrow.EVENT_CARD_ID_FK + +CREATE TRIGGER TRIG_CARD_TIME_STATS_DEL AFTER DELETE ON LA_EVENT + REFERENCING OLD ROW AS oldrow + FOR EACH ROW + UPDATE LA_CARD SET CARD_LAST_UPDATED = NOW(), CARD_LAST_UPDATED_USER_ID_FK = oldrow.EVENT_USER_ID_FK WHERE CARD_ID = oldrow.EVENT_CARD_ID_FK + +-- VIEWS +CREATE VIEW LA_CARD_WITH_BOARD_ID AS ( + SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER, BOARD_COLUMN_BOARD_ID_FK "BOARD_ID", BOARD_COLUMN_LOCATION + FROM LA_CARD INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK); + +CREATE VIEW LA_CARD_DATA_FULL AS ( + SELECT CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_TYPE, CARD_DATA_CONTENT, CARD_DATA_ORDER, CARD_DATA_DELETED, EVENT_TIME, EVENT_TYPE, EVENT_PREV_CARD_DATA_ID_FK, EVENT_USER_ID_FK + FROM LA_CARD_DATA INNER JOIN LA_EVENT ON LA_CARD_DATA.CARD_DATA_ID = LA_EVENT.EVENT_CARD_DATA_ID_FK); + +CREATE VIEW LA_CARD_DATA_COUNT AS ( + SELECT BOARD_ID, CARD_ID, CARD_BOARD_COLUMN_ID_FK ,CARD_DATA_TYPE, COUNT(CARD_DATA_TYPE) AS CARD_DATA_TYPE_COUNT FROM LA_CARD_WITH_BOARD_ID INNER JOIN LA_CARD_DATA ON LA_CARD_DATA.CARD_DATA_CARD_ID_FK = CARD_ID + WHERE CARD_DATA_DELETED = FALSE + GROUP BY BOARD_ID, CARD_ID, CARD_BOARD_COLUMN_ID_FK, CARD_DATA_TYPE +); + +CREATE VIEW LA_CARD_DATA_UPLOAD_CONTENT_LIGHT AS ( + SELECT EVENT_USER_ID_FK, EVENT_TIME, CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_CONTENT, CARD_DATA_DELETED, DISPLAYED_NAME, SIZE, CONTENT_TYPE + FROM LA_CARD_DATA INNER JOIN LA_CARD_DATA_UPLOAD ON LA_CARD_DATA.CARD_DATA_ID = LA_CARD_DATA_UPLOAD.CARD_DATA_ID_FK + INNER JOIN LA_CARD_DATA_UPLOAD_CONTENT ON LA_CARD_DATA_UPLOAD.CARD_DATA_UPLOAD_CONTENT_DIGEST_FK = LA_CARD_DATA_UPLOAD_CONTENT.DIGEST + INNER JOIN LA_EVENT ON EVENT_CARD_DATA_ID_FK = CARD_DATA_ID + WHERE CARD_DATA_TYPE = 'FILE' AND EVENT_TYPE = 'FILE_UPLOAD' AND CARD_DATA_DELETED = FALSE); + +CREATE VIEW LA_CARD_FULL AS ( + SELECT LA_CARD.CARD_ID AS CARD_ID, LA_CARD.CARD_NAME AS CARD_NAME, LA_CARD.CARD_SEQ_NUMBER AS CARD_SEQ_NUMBER, LA_CARD.CARD_BOARD_COLUMN_ID_FK AS CARD_BOARD_COLUMN_ID_FK, LA_CARD.CARD_ORDER AS CARD_ORDER, LA_EVENT.EVENT_TIME AS CREATE_TIME, LA_EVENT.EVENT_USER_ID_FK AS CREATE_USER, + LA_CARD.CARD_LAST_UPDATED AS LAST_UPDATE_TIME, LA_CARD.CARD_LAST_UPDATED_USER_ID_FK AS LAST_UPDATE_USER, PROJECT_SHORT_NAME, PROJECT_ID, BOARD_SHORT_NAME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_LOCATION FROM LA_CARD + INNER JOIN LA_EVENT ON LA_CARD.CARD_ID = LA_EVENT.EVENT_CARD_ID_FK AND LA_EVENT.EVENT_TYPE = 'CARD_CREATE' + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD ON LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID + INNER JOIN LA_PROJECT ON LA_PROJECT.PROJECT_ID = LA_BOARD.BOARD_PROJECT_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID); + +CREATE VIEW LA_ASSIGNED_CARD AS ( + SELECT LA_CARD.CARD_ID AS ASSIGNED_CARD_ID, LA_CARD.CARD_LAST_UPDATED AS ASSIGNED_EVENT_TIME, CARD_LABEL_VALUE_USER_FK AS ASSIGNED_USER_ID, BOARD_COLUMN_DEFINITION_VALUE AS ASSIGNED_CARD_STATUS FROM LA_CARD + INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID + INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID + WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'SYSTEM' AND LA_CARD_LABEL.CARD_LABEL_NAME = 'ASSIGNED' + AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE +); + +CREATE VIEW LA_ASSIGNED_CARD_PROJECT AS ( + SELECT LA_CARD.CARD_ID AS ASSIGNED_CARD_ID, LA_CARD.CARD_LAST_UPDATED AS ASSIGNED_EVENT_TIME, CARD_LABEL_VALUE_USER_FK AS ASSIGNED_USER_ID, BOARD_COLUMN_DEFINITION_VALUE AS ASSIGNED_CARD_STATUS, LA_PROJECT.PROJECT_SHORT_NAME AS ASSIGNED_PROJECT_SHORT_NAME FROM LA_CARD + INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID + INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID + INNER JOIN LA_PROJECT ON LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_PROJECT_ID_FK = LA_PROJECT.PROJECT_ID + WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'SYSTEM' AND LA_CARD_LABEL.CARD_LABEL_NAME = 'ASSIGNED' + AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE +); + + +CREATE VIEW LA_BOARD_COLUMN_INFO AS ( + SELECT PROJECT_ID, PROJECT_NAME, BOARD_ID, BOARD_NAME, BOARD_SHORT_NAME, BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR + FROM LA_BOARD_COLUMN INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID + INNER JOIN LA_PROJECT ON BOARD_PROJECT_ID_FK = PROJECT_ID + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID = BOARD_COLUMN_DEFINITION_ID_FK +); + + +CREATE VIEW LA_BOARD_COLUMN_FULL AS ( + SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR + FROM LA_BOARD_COLUMN INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID +); + + +-- CUSTOM SEARCH FUNCTION + CREATE FUNCTION LA_TEXT_SEARCH(data LONGVARCHAR, toSearch LONGVARCHAR) RETURNS BOOLEAN + LANGUAGE JAVA DETERMINISTIC NO SQL + EXTERNAL NAME 'CLASSPATH:io.lavagna.service.SearchService.searchText' +CREATE FUNCTION LA_TEXT_SEARCH_CLOB(data CLOB, toSearch LONGVARCHAR) RETURNS BOOLEAN + LANGUAGE JAVA DETERMINISTIC NO SQL + EXTERNAL NAME 'CLASSPATH:io.lavagna.service.SearchService.searchTextClob' + + + +-- DEFAULT DATA +-- ANONYMOUS USER DEFAULT ROLES AND PERMISSION +INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('system', 'anonymous'); +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES('ANONYMOUS', FALSE, TRUE, TRUE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ANONYMOUS'), 'READ'); +-- ADMIN AND DEFAULT ROLES AND PERMISSION +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES('ADMIN', FALSE, FALSE, TRUE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'ADMINISTRATION'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'SEARCH'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_PROFILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'PROJECT_ADMINISTRATION'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_PROJECT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'RENAME_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'READ'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'TOGGLE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'ORDER_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MANAGE_LABEL_VALUE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_FILE'); + + +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE) VALUES('DEFAULT', FALSE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'READ'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'SEARCH'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_PROFILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MOVE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'TOGGLE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MOVE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'ORDER_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MANAGE_LABEL_VALUE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_CARD_COMMENT'); + + +-- +INSERT INTO LA_PROJECT(PROJECT_NAME, PROJECT_SHORT_NAME) VALUES ('Default','DEFAULT'); +-- DEFAULT ANON ROLE +INSERT INTO LA_PROJECT_ROLE(PROJECT_ROLE_NAME, PROJECT_ID_FK, PROJECT_ROLE_HIDDEN, PROJECT_ROLE_READONLY, PROJECT_ROLE_REMOVABLE) VALUES('ANONYMOUS', (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, TRUE, FALSE); +INSERT INTO LA_PROJECT_ROLE_PERMISSION(PROJECT_ROLE_ID_FK, PERMISSION) VALUES (IDENTITY(), 'READ'); +-- DEFAULT LABELS FOR Default PROJECT +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), FALSE, 'USER', 'SYSTEM', 'ASSIGNED', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, 'TIMESTAMP', 'SYSTEM', 'DUE_DATE', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, 'LIST', 'SYSTEM', 'MILESTONE', 0); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT')), 0, 'Unplanned'); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), FALSE, 'USER', 'SYSTEM', 'WATCHED_BY', 0); +--DEFAULT COLUMN DEFINITION FOR Default PROJECT +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'OPEN', 14242639); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'CLOSED', 6076508); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'BACKLOG', 4361162); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'DEFERRED', 15773006); + diff --git a/src/main/resources/io/lavagna/db/HSQLDB/V2__ADD_DATA.sql b/src/main/resources/io/lavagna/db/HSQLDB/V2__ADD_DATA.sql new file mode 100644 index 000000000..17ec2c416 --- /dev/null +++ b/src/main/resources/io/lavagna/db/HSQLDB/V2__ADD_DATA.sql @@ -0,0 +1,180 @@ +-- +-- This file is part of lavagna. +-- +-- lavagna 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. +-- +-- lavagna 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 lavagna. If not, see . +-- + +-- DEMO CONF PARAMETER +INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('SETUP_COMPLETE', 'true'); +INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('BASE_APPLICATION_URL', 'http://localhost:8080/'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('BASE_APPLICATION_URL', 'http://localhost:8080/test/'); +INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('AUTHENTICATION_METHOD', '["DEMO"]'); + +--200kb max file size +INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('MAX_UPLOAD_FILE_SIZE', '204800'); + +INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('TRELLO_API_KEY', 'd9e452d67d0e4407b9077e15df9dfd5a'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('ENABLE_ANON_USER', 'true'); + + +-- +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('AUTHENTICATION_METHOD', '["OAUTH"]'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('OAUTH_CONFIGURATION', '{ "baseUrl" : "http://localhost:8080", "providers" : [{"provider" : "bitbucket", "apiKey" : "", "apiSecret" : ""},{"provider" : "github", "apiKey" : "", "apiSecret" : ""}, {"provider" : "google", "apiKey": "", "apiSecret" : ""}]}'); +-- + +-- +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('AUTHENTICATION_METHOD', '["PERSONA"]'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('PERSONA_AUDIENCE', 'http://localhost:8080'); +-- + +-- +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('AUTHENTICATION_METHOD', '["LDAP"]'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('LDAP_SERVER_URL', 'ldap://localhost:10389'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('LDAP_MANAGER_DN', 'uid=admin,ou=system'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('LDAP_MANAGER_PASSWORD', 'secret'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('LDAP_USER_SEARCH_BASE', 'ou=system'); +--INSERT INTO LA_CONF(CONF_KEY, CONF_VALUE) VALUES ('LDAP_USER_SEARCH_FILTER', 'uid={0}'); +-- + +-- TEST USER +INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('demo', 'user'); +INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('demo', 'user1'); +INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('demo', 'user2'); +--INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('ldap', 'user'); +--INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('persona', 'sylvain.jermini@syjer.com'); +--INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('oauth.google', 'sylvain.jermini@syjer.com'); +--INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('oauth.github', 'syjer'); +--INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('oauth.bitbucket', 'syjer'); + +-- TEST PROJECT +INSERT INTO LA_PROJECT(PROJECT_NAME, PROJECT_SHORT_NAME, PROJECT_DESCRIPTION) VALUES ('test', 'TEST', 'Test Project'); + +-- DEFAULT ANON ROLE +INSERT INTO LA_PROJECT_ROLE(PROJECT_ROLE_NAME, PROJECT_ID_FK, PROJECT_ROLE_HIDDEN, PROJECT_ROLE_READONLY, PROJECT_ROLE_REMOVABLE) VALUES('ANONYMOUS', (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, TRUE, FALSE); +INSERT INTO LA_PROJECT_ROLE_PERMISSION(PROJECT_ROLE_ID_FK, PERMISSION) VALUES (IDENTITY(), 'READ'); + +-- DEFAULT LABELS FOR TEST PROJECT +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), FALSE, 'USER', 'SYSTEM', 'ASSIGNED', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'TIMESTAMP', 'SYSTEM', 'DUE_DATE', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'LIST', 'SYSTEM', 'MILESTONE', 0); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 0, '1.0.0'); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 1, 'Unplanned'); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), FALSE, 'USER', 'SYSTEM', 'WATCHED_BY', 0); + +-- DEFAULT COLUMN DEFINITION +INSERT INTO LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), 'OPEN', 14242639); +INSERT INTO LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), 'CLOSED', 6076508); +INSERT INTO LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), 'BACKLOG', 4361162); +INSERT INTO LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), 'DEFERRED', 15773006); + +-- TEST BOARD +INSERT INTO LA_BOARD(BOARD_PROJECT_ID_FK, BOARD_NAME, BOARD_SHORT_NAME) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'),'test', 'TEST'); +INSERT INTO LA_BOARD_COUNTER(BOARD_COUNTER_ID_FK, BOARD_COUNTER_CARD_SEQUENCE) VALUES(IDENTITY() , 1); + +-- TEST COLUMNS +INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES ('New', 0, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), 'BOARD', (SELECT BOARD_COLUMN_DEFINITION_ID FROM LA_BOARD_COLUMN_DEFINITION JOIN LA_PROJECT ON PROJECT_ID = BOARD_COLUMN_DEFINITION_PROJECT_ID_FK WHERE BOARD_COLUMN_DEFINITION_VALUE = 'OPEN' AND PROJECT_SHORT_NAME = 'TEST')); +INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES ('Done', 1, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), 'BOARD', (SELECT BOARD_COLUMN_DEFINITION_ID FROM LA_BOARD_COLUMN_DEFINITION JOIN LA_PROJECT ON PROJECT_ID = BOARD_COLUMN_DEFINITION_PROJECT_ID_FK WHERE BOARD_COLUMN_DEFINITION_VALUE = 'CLOSED' AND PROJECT_SHORT_NAME = 'TEST')); +INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES ('ARCHIVE', 0, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), 'ARCHIVE', (SELECT BOARD_COLUMN_DEFINITION_ID FROM LA_BOARD_COLUMN_DEFINITION JOIN LA_PROJECT ON PROJECT_ID = BOARD_COLUMN_DEFINITION_PROJECT_ID_FK WHERE BOARD_COLUMN_DEFINITION_VALUE = 'CLOSED' AND PROJECT_SHORT_NAME = 'TEST')); +INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES ('BACKLOG', 0, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), 'BACKLOG', (SELECT BOARD_COLUMN_DEFINITION_ID FROM LA_BOARD_COLUMN_DEFINITION JOIN LA_PROJECT ON PROJECT_ID = BOARD_COLUMN_DEFINITION_PROJECT_ID_FK WHERE BOARD_COLUMN_DEFINITION_VALUE = 'BACKLOG' AND PROJECT_SHORT_NAME = 'TEST')); +INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES ('TRASH', 0, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), 'TRASH', (SELECT BOARD_COLUMN_DEFINITION_ID FROM LA_BOARD_COLUMN_DEFINITION JOIN LA_PROJECT ON PROJECT_ID = BOARD_COLUMN_DEFINITION_PROJECT_ID_FK WHERE BOARD_COLUMN_DEFINITION_VALUE = 'CLOSED' AND PROJECT_SHORT_NAME = 'TEST')); +INSERT INTO LA_BOARD_COLUMN(BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_ID_FK) VALUES ('Deferred', 2, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), 'BOARD', (SELECT BOARD_COLUMN_DEFINITION_ID FROM LA_BOARD_COLUMN_DEFINITION JOIN LA_PROJECT ON PROJECT_ID = BOARD_COLUMN_DEFINITION_PROJECT_ID_FK WHERE BOARD_COLUMN_DEFINITION_VALUE = 'DEFERRED' AND PROJECT_SHORT_NAME = 'TEST')); + +-- TEST CARDS +UPDATE LA_BOARD_COUNTER SET BOARD_COUNTER_CARD_SEQUENCE = 9 WHERE BOARD_COUNTER_ID_FK = 0; +INSERT INTO LA_CARD VALUES(0,'Card1',0,1,1, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(1,'Card2',0,2,2, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(2,'Card3',1,1,3, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(3,'Archived Card',2,1,4, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(4,'Backlog card',3,1,5, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(5,'Card4',1,2,6, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(6,'Card5',5,3,7, 1, NOW(), 1); +INSERT INTO LA_CARD VALUES(7,'Card6',5,3,8, 1, NOW(), 1); + +-- TEST LABELS +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES + ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'STRING', 'USER','Type', 52224); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(0, TRUE, FALSE, 2, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'Type'), 'STRING', 'Feature', NULL, NULL, NULL, NULL, NULL); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(1, TRUE, FALSE, 1, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 'LIST', NULL, NULL, NULL, NULL, NULL, (SELECT CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = '1.0.0')); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(2, TRUE, FALSE, 2, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 'LIST', NULL, NULL, NULL, NULL, NULL, (SELECT CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = '1.0.0')); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(3, TRUE, FALSE, 5, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 'LIST', NULL, NULL, NULL, NULL, NULL, (SELECT CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = '1.0.0')); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(4, TRUE, FALSE, 6, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 'LIST', NULL, NULL, NULL, NULL, NULL, (SELECT CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = '1.0.0')); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(5, TRUE, FALSE, 7, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 'LIST', NULL, NULL, NULL, NULL, NULL, (SELECT CARD_LABEL_LIST_VALUE_ID FROM LA_CARD_LABEL_LIST_VALUE WHERE CARD_LABEL_LIST_VALUE = '1.0.0')); +INSERT INTO LA_CARD_LABEL_VALUE VALUES(6, TRUE, FALSE, 1, (SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'Type'), 'STRING', 'Bugfixing', NULL, NULL, NULL, NULL, NULL); + +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'NULL', 'USER','Bug', 15012864); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'INT', 'USER','Effort', 22000); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), FALSE, 'CARD', 'USER','Duplicate of', 32000); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'USER', 'USER','Reviewer', 15767049); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'TIMESTAMP', 'USER','Deadline', 13556480); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST'), TRUE, 'LIST', 'USER','Priority', 52200); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'Priority' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 0, 'Low'); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'Priority' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'TEST')), 1, 'High'); + +-- ADD A GLOBAL ROLE READ_ONLY +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE) VALUES('READ_ONLY', FALSE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'READ_ONLY'), 'READ'); + +-- ADD user 'user' the role 'ADMIN' +INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'user' AND USER_PROVIDER = 'demo'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN')); +INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'user1' AND USER_PROVIDER = 'demo'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT')); +INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'user2' AND USER_PROVIDER = 'demo'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'READ_ONLY')); + + +--INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +--((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'user' AND USER_PROVIDER = 'ldap'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN')); +--INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +--((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'sylvain.jermini@syjer.com' AND USER_PROVIDER = 'persona'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN')); +--INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +--((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'sylvain.jermini@syjer.com' AND USER_PROVIDER = 'oauth.google'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN')); +--INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +--((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'syjer' AND USER_PROVIDER = 'oauth.github'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN')); +--INSERT INTO LA_USER_ROLE(USER_ID_FK, ROLE_ID_FK) VALUES +--((SELECT USER_ID FROM LA_USER WHERE USER_NAME = 'syjer' AND USER_PROVIDER = 'oauth.bitbucket'), (SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN')); + + +-- CARD EVENTS +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (0, 1, 'CARD_CREATE', NOW(), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New'), 'Card1'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (1, 1, 'CARD_CREATE', NOW(), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New'), 'Card2'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (2, 1, 'CARD_CREATE', NOW(), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Done'), 'Card3'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (3, 1, 'CARD_CREATE', NOW(), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'ARCHIVE'), 'Archived Card'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (4, 1, 'CARD_CREATE', NOW(), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'BACKLOG'), 'Backlog card'); + +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (5, 1, 'CARD_CREATE', NOW() - INTERVAL 3 DAY, (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New'), 'Card4'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK) VALUES (5, 1, 'CARD_MOVE', NOW() - INTERVAL 2 DAY, (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Done'), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New')); + +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (6, 1, 'CARD_CREATE', NOW() - INTERVAL 2 DAY, (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New'), 'Card5'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK) VALUES (6, 1, 'CARD_MOVE', NOW() - INTERVAL 1 DAY, (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Deferred'), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New')); + +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_VALUE_STRING) VALUES (7, 1, 'CARD_CREATE', NOW() - INTERVAL 2 DAY, (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New'), 'Card6'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK) VALUES (7, 1, 'CARD_MOVE', NOW() - INTERVAL 1 DAY, (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Done'), (SELECT BOARD_COLUMN_ID FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New')); + +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK) VALUES (3, 1, 'CARD_ARCHIVE', NOW(), 2, 0); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_COLUMN_ID_FK, EVENT_PREV_COLUMN_ID_FK) VALUES (4, 1, 'CARD_BACKLOG', NOW(), 3, 0); + +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_VALUE_STRING) VALUES (2, 1,'LABEL_CREATE',NOW(), 'Type', 'STRING', 'Feature'); +INSERT INTO LA_EVENT (EVENT_CARD_ID_FK, EVENT_USER_ID_FK, EVENT_TYPE, EVENT_TIME, EVENT_LABEL_NAME, EVENT_LABEL_TYPE, EVENT_VALUE_STRING) VALUES (1, 1,'LABEL_CREATE',NOW(), 'Type', 'STRING', 'Bugfixing'); + +-- BOARD STATISTICS +INSERT INTO LA_BOARD_STATISTICS VALUES (NOW() - INTERVAL 2 DAY, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), (SELECT BOARD_COLUMN_DEFINITION_ID_FK FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Done'), 'BOARD', 1); + +INSERT INTO LA_BOARD_STATISTICS VALUES (NOW() - INTERVAL 1 DAY, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), (SELECT BOARD_COLUMN_DEFINITION_ID_FK FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Done'), 'BOARD', 1); +INSERT INTO LA_BOARD_STATISTICS VALUES (NOW() - INTERVAL 1 DAY, (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), (SELECT BOARD_COLUMN_DEFINITION_ID_FK FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Deferred'), 'BOARD', 1); + +INSERT INTO LA_BOARD_STATISTICS VALUES (NOW(), (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), (SELECT BOARD_COLUMN_DEFINITION_ID_FK FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'New'), 'BOARD', 2); +INSERT INTO LA_BOARD_STATISTICS VALUES (NOW(), (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), (SELECT BOARD_COLUMN_DEFINITION_ID_FK FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Done'), 'BOARD', 2); +INSERT INTO LA_BOARD_STATISTICS VALUES (NOW(), (SELECT BOARD_ID FROM LA_BOARD WHERE BOARD_NAME = 'test'), (SELECT BOARD_COLUMN_DEFINITION_ID_FK FROM LA_BOARD_COLUMN JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID WHERE BOARD_NAME = 'test' AND BOARD_COLUMN_NAME = 'Deferred'), 'BOARD', 1); \ No newline at end of file diff --git a/src/main/resources/io/lavagna/db/MYSQL/V1__INITIAL_VERSION.sql b/src/main/resources/io/lavagna/db/MYSQL/V1__INITIAL_VERSION.sql new file mode 100644 index 000000000..7c7630f1e --- /dev/null +++ b/src/main/resources/io/lavagna/db/MYSQL/V1__INITIAL_VERSION.sql @@ -0,0 +1,763 @@ +-- +-- This file is part of lavagna. +-- +-- lavagna 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. +-- +-- lavagna 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 lavagna. If not, see . +-- + +-- CONFIGURATION TABLE +CREATE TABLE LA_CONF ( + CONF_KEY VARCHAR(64) PRIMARY KEY NOT NULL, + CONF_VALUE MEDIUMTEXT NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; + +-- USER +CREATE TABLE LA_USER ( + USER_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + USER_PROVIDER VARCHAR(16) NOT NULL, + USER_NAME VARCHAR(128) NOT NULL, + USER_EMAIL VARCHAR(256), + USER_ENABLED BOOLEAN, + USER_DISPLAY_NAME VARCHAR(256), + USER_EMAIL_NOTIFICATION BOOLEAN DEFAULT TRUE NOT NULL, + USER_LAST_EMAIL_SENT TIMESTAMP NULL DEFAULT NULL, + USER_LAST_CHECKPOINT_COUNT INTEGER DEFAULT 0 NOT NULL, + USER_LAST_CHECKED TIMESTAMP NULL DEFAULT NULL, + USER_MEMBER_SINCE TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- CONSTRAINTS +ALTER TABLE LA_USER ADD CONSTRAINT UNIQUE_LA_USER_USER_NAME UNIQUE(USER_PROVIDER, USER_NAME); + +-- USER REMEMBER +CREATE TABLE LA_USER_REMEMBER ( + USER_REMEMBER_HASHED_TOKEN CHAR(64) NOT NULL, + USER_REMEMBER_ID_FK INTEGER NOT NULL, + USER_REMEMBER_LAST_USE TIMESTAMP NOT NULL, + PRIMARY KEY(USER_REMEMBER_ID_FK, USER_REMEMBER_HASHED_TOKEN) +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- CONSTRAINTS +ALTER TABLE LA_USER_REMEMBER ADD FOREIGN KEY(USER_REMEMBER_ID_FK) REFERENCES LA_USER(USER_ID); + +-- PROJECT +CREATE TABLE LA_PROJECT ( + PROJECT_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + PROJECT_NAME VARCHAR(128) NOT NULL, + PROJECT_SHORT_NAME VARCHAR(8) NOT NULL, + PROJECT_ARCHIVED BOOLEAN DEFAULT FALSE NOT NULL, + PROJECT_DESCRIPTION MEDIUMTEXT +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_PROJECT ADD CONSTRAINT UNIQUE_LA_PROJECT_SHORT_NAME UNIQUE(PROJECT_SHORT_NAME); + +-- BOARDS +CREATE TABLE LA_BOARD ( + BOARD_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + BOARD_PROJECT_ID_FK INTEGER NOT NULL, + BOARD_NAME VARCHAR(128) NOT NULL, + BOARD_SHORT_NAME VARCHAR(8) NOT NULL, + BOARD_ARCHIVED BOOLEAN DEFAULT FALSE NOT NULL, + BOARD_DESCRIPTION MEDIUMTEXT +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- CONSTRAINTS +ALTER TABLE LA_BOARD ADD CONSTRAINT UNIQUE_LA_BOARD_SHORT_NAME UNIQUE(BOARD_SHORT_NAME); +ALTER TABLE LA_BOARD ADD FOREIGN KEY(BOARD_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); + +-- BOARD COUNTERS +CREATE TABLE LA_BOARD_COUNTER ( + BOARD_COUNTER_ID_FK INTEGER PRIMARY KEY NOT NULL, + BOARD_COUNTER_CARD_SEQUENCE INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_BOARD_COUNTER ADD FOREIGN KEY(BOARD_COUNTER_ID_FK) REFERENCES LA_BOARD(BOARD_ID); + +-- BOARD DEFINITION, FOR STATISTICS AND MICROMANAGEMENT +CREATE TABLE LA_BOARD_COLUMN_DEFINITION ( + BOARD_COLUMN_DEFINITION_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + BOARD_COLUMN_DEFINITION_PROJECT_ID_FK INTEGER NOT NULL, + BOARD_COLUMN_DEFINITION_VALUE ENUM('OPEN', 'CLOSED', 'BACKLOG', 'DEFERRED') NOT NULL, + BOARD_COLUMN_DEFINITION_COLOR INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- CONSTRAINTS: +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD FOREIGN KEY(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD CONSTRAINT BOARD_COLUMN_DEFINITION_COLOR_UNIQUE UNIQUE(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_COLOR); + +-- BOARD COLUMN, FOR CATEGORIZING A CARD +CREATE TABLE LA_BOARD_COLUMN ( + BOARD_COLUMN_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + BOARD_COLUMN_NAME VARCHAR(128) NOT NULL, + BOARD_COLUMN_ORDER INTEGER NOT NULL, + BOARD_COLUMN_LOCATION ENUM('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH') NOT NULL, + BOARD_COLUMN_BOARD_ID_FK INTEGER NOT NULL, + BOARD_COLUMN_DEFINITION_ID_FK INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- CONSTRAINTS: +ALTER TABLE LA_BOARD_COLUMN ADD FOREIGN KEY(BOARD_COLUMN_BOARD_ID_FK) REFERENCES LA_BOARD(BOARD_ID); +ALTER TABLE LA_BOARD_COLUMN ADD FOREIGN KEY(BOARD_COLUMN_DEFINITION_ID_FK) REFERENCES LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_ID); + +DELIMITER // +CREATE FUNCTION LA_FUN_BOARD_COLUMN_ENFORCE (BOARD_COLUMN_NAME VARCHAR(128), BOARD_COLUMN_LOCATION ENUM('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH')) + RETURNS BOOLEAN + DETERMINISTIC + BEGIN + RETURN BOARD_COLUMN_LOCATION IN ('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH') AND + (CASE + WHEN BOARD_COLUMN_LOCATION = 'BOARD' AND BOARD_COLUMN_NAME IN ('BACKLOG', 'ARCHIVE', 'TRASH') THEN FALSE + ELSE TRUE + END); + END// + +CREATE TRIGGER CHECK_LA_BOARD_COLUMN_INSERT BEFORE INSERT ON LA_BOARD_COLUMN + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_BOARD_COLUMN_ENFORCE(NEW.BOARD_COLUMN_NAME, NEW.BOARD_COLUMN_LOCATION)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + +CREATE TRIGGER CHECK_LA_BOARD_COLUMN_UPDATE BEFORE UPDATE ON LA_BOARD_COLUMN + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_BOARD_COLUMN_ENFORCE(NEW.BOARD_COLUMN_NAME, NEW.BOARD_COLUMN_LOCATION)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// +DELIMITER ; + +-- BOARD STATISTICS +CREATE TABLE LA_BOARD_STATISTICS ( + BOARD_STATISTICS_TIME TIMESTAMP NOT NULL, + BOARD_STATISTICS_BOARD_ID_FK INTEGER NOT NULL, + BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK INTEGER NOT NULL, + BOARD_STATISTICS_LOCATION ENUM('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH') NOT NULL, + BOARD_STATISTICS_COUNT INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_BOARD_STATISTICS ADD FOREIGN KEY(BOARD_STATISTICS_BOARD_ID_FK) REFERENCES LA_BOARD(BOARD_ID); +ALTER TABLE LA_BOARD_STATISTICS ADD FOREIGN KEY(BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK) REFERENCES LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_ID); + +-- LATEST BOARD STATISTICS BY DAY +CREATE VIEW LA_BOARD_STATISTICS_DAYS AS (SELECT MAX(BOARD_STATISTICS_TIME) AS DAY FROM LA_BOARD_STATISTICS GROUP BY CAST(BOARD_STATISTICS_TIME AS DATE)); + +-- CARD +CREATE TABLE LA_CARD ( + CARD_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + CARD_NAME VARCHAR(255) NOT NULL, + CARD_BOARD_COLUMN_ID_FK INTEGER NOT NULL, + CARD_ORDER INTEGER NOT NULL, + CARD_SEQ_NUMBER INTEGER NOT NULL, + CARD_USER_ID_FK INTEGER NOT NULL, + CARD_LAST_UPDATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CARD_LAST_UPDATED_USER_ID_FK INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; + + +-- support table for FTS +CREATE TABLE LA_CARD_FTS_SUPPORT ( + CARD_FTS_SUPPORT_CARD_ID_FK INTEGER PRIMARY KEY NOT NULL, + CARD_FTS_SUPPORT_CARD_NAME VARCHAR(255) NOT NULL, + CARD_FTS_SUPPORT_LAST_UPDATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FULLTEXT(CARD_FTS_SUPPORT_CARD_NAME) +) ENGINE=MyISAM CHARACTER SET=utf8 COLLATE utf8_general_ci; +-- CONSTRAINTS: +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_BOARD_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_LAST_UPDATED_USER_ID_FK) REFERENCES LA_USER(USER_ID); + +-- +CREATE TABLE LA_CARD_DATA ( + CARD_DATA_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + CARD_DATA_CARD_ID_FK INTEGER NOT NULL, + CARD_DATA_REFERENCE_ID INTEGER, + CARD_DATA_DELETED BOOLEAN DEFAULT FALSE NOT NULL, + CARD_DATA_TYPE ENUM('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'FILE', 'COMMENT_HISTORY', 'DESCRIPTION', 'DESCRIPTION_HISTORY') NOT NULL, + CARD_DATA_CONTENT MEDIUMTEXT NOT NULL, + CARD_DATA_ORDER INTEGER NOT NULL, + CARD_DATA_LAST_UPDATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- support table for FTS +CREATE TABLE LA_CARD_DATA_FTS_SUPPORT ( + CARD_DATA_FTS_SUPPORT_CARD_DATA_ID_FK INTEGER PRIMARY KEY NOT NULL, + CARD_DATA_FTS_SUPPORT_CARD_DATA_CONTENT MEDIUMTEXT NOT NULL, + CARD_DATA_FTS_SUPPORT_LAST_UPDATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FULLTEXT(CARD_DATA_FTS_SUPPORT_CARD_DATA_CONTENT) +) ENGINE=MyISAM CHARACTER SET=utf8 COLLATE utf8_general_ci; + +ALTER TABLE LA_CARD_DATA ADD FOREIGN KEY(CARD_DATA_CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_DATA ADD FOREIGN KEY(CARD_DATA_REFERENCE_ID) REFERENCES LA_CARD_DATA(CARD_DATA_ID); + +DELIMITER // +CREATE FUNCTION LA_FUN_CARD_DATA_ENFORCE (CARD_DATA_TYPE ENUM('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'FILE', 'COMMENT_HISTORY', 'DESCRIPTION', 'DESCRIPTION_HISTORY'), CARD_DATA_REFERENCE_ID INTEGER) + RETURNS BOOLEAN + DETERMINISTIC + BEGIN + RETURN CARD_DATA_TYPE IN ('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'FILE', 'COMMENT_HISTORY', 'DESCRIPTION', 'DESCRIPTION_HISTORY') AND + (CASE + WHEN CARD_DATA_TYPE IN ('COMMENT','ACTION_LIST', 'DESCRIPTION') THEN CARD_DATA_REFERENCE_ID IS NULL + WHEN CARD_DATA_TYPE IN ('ACTION_CHECKED', 'ACTION_UNCHECKED', 'COMMENT_HISTORY', 'DESCRIPTION_HISTORY') THEN CARD_DATA_REFERENCE_ID IS NOT NULL + ELSE TRUE + END); + END// + +CREATE TRIGGER CHECK_LA_CARD_DATA_INSERT BEFORE INSERT ON LA_CARD_DATA + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_CARD_DATA_ENFORCE(NEW.CARD_DATA_TYPE, NEW.CARD_DATA_REFERENCE_ID)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + +CREATE TRIGGER CHECK_LA_CARD_DATA_UPDATE BEFORE UPDATE ON LA_CARD_DATA + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_CARD_DATA_ENFORCE(NEW.CARD_DATA_TYPE, NEW.CARD_DATA_REFERENCE_ID)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + SET NEW.CARD_DATA_LAST_UPDATED = NOW(); + END// + +DELIMITER ; + + +-- DATA UPLOAD +CREATE TABLE LA_CARD_DATA_UPLOAD_CONTENT ( + DIGEST CHAR(64) NOT NULL, + SIZE INTEGER NOT NULL, + CONTENT MEDIUMBLOB NOT NULL, + CONTENT_TYPE VARCHAR(255) NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_CARD_DATA_UPLOAD_CONTENT ADD CONSTRAINT UNIQUE_LA_CARD_UPLOAD_CONTENT UNIQUE(DIGEST); + +CREATE TABLE LA_CARD_DATA_UPLOAD ( + CARD_DATA_ID_FK INTEGER NOT NULL, + CARD_DATA_UPLOAD_CONTENT_DIGEST_FK CHAR(64) NOT NULL, + ORIGINAL_NAME VARCHAR(255) NOT NULL, + DISPLAYED_NAME VARCHAR(255) NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_CARD_DATA_UPLOAD ADD FOREIGN KEY(CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_CARD_DATA_UPLOAD ADD FOREIGN KEY(CARD_DATA_UPLOAD_CONTENT_DIGEST_FK) REFERENCES LA_CARD_DATA_UPLOAD_CONTENT(DIGEST); + +-- CARD LABEL +CREATE TABLE LA_CARD_LABEL ( + CARD_LABEL_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + CARD_LABEL_PROJECT_ID_FK INTEGER NOT NULL, + CARD_LABEL_UNIQUE BOOLEAN DEFAULT FALSE NOT NULL, + CARD_LABEL_TYPE ENUM('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST') NOT NULL, + CARD_LABEL_DOMAIN ENUM('SYSTEM', 'USER') NOT NULL, + CARD_LABEL_NAME VARCHAR(32) NOT NULL, + CARD_LABEL_COLOR INTEGER +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_CARD_LABEL ADD FOREIGN KEY(CARD_LABEL_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT UNIQUE_LA_CARD_LABEL_PROJECT_ID_FK_NAME UNIQUE(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_NAME); +-- + +DELIMITER // + +CREATE FUNCTION LA_FUN_CARD_LABEL_ENFORCE ( + CARD_LABEL_TYPE ENUM('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST'), + CARD_LABEL_DOMAIN ENUM('SYSTEM', 'USER')) + RETURNS BOOLEAN + DETERMINISTIC + BEGIN + RETURN CARD_LABEL_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST') AND CARD_LABEL_DOMAIN IN ('SYSTEM', 'USER'); + END// + +CREATE TRIGGER CHECK_LA_CARD_LABEL_INSERT BEFORE INSERT ON LA_CARD_LABEL + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_CARD_LABEL_ENFORCE(NEW.CARD_LABEL_TYPE, NEW.CARD_LABEL_DOMAIN)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + +CREATE TRIGGER CHECK_LA_CARD_LABEL_UPDATE BEFORE UPDATE ON LA_CARD_LABEL + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_CARD_LABEL_ENFORCE(NEW.CARD_LABEL_TYPE, NEW.CARD_LABEL_DOMAIN)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + +DELIMITER ; + +-- junction table label <-> list values +CREATE TABLE LA_CARD_LABEL_LIST_VALUE ( + CARD_LABEL_LIST_VALUE_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + CARD_LABEL_ID_FK INTEGER NOT NULL, + CARD_LABEL_LIST_VALUE_ORDER INTEGER NOT NULL, + CARD_LABEL_LIST_VALUE VARCHAR(255) +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_CARD_LABEL_LIST_VALUE ADD FOREIGN KEY(CARD_LABEL_ID_FK) REFERENCES LA_CARD_LABEL(CARD_LABEL_ID); +-- + +-- junction table label <-> card +CREATE TABLE LA_CARD_LABEL_VALUE ( + CARD_LABEL_VALUE_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + CARD_LABEL_VALUE_USE_UNIQUE_INDEX BOOLEAN DEFAULT NULL, + CARD_LABEL_VALUE_DELETED BOOLEAN DEFAULT FALSE NOT NULL, + CARD_ID_FK INTEGER NOT NULL, + CARD_LABEL_ID_FK INTEGER NOT NULL, + CARD_LABEL_VALUE_TYPE ENUM('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST') NOT NULL, + CARD_LABEL_VALUE_STRING VARCHAR(32), + CARD_LABEL_VALUE_TIMESTAMP TIMESTAMP NULL DEFAULT NULL, + CARD_LABEL_VALUE_INT INTEGER, + CARD_LABEL_VALUE_CARD_FK INTEGER, + CARD_LABEL_VALUE_USER_FK INTEGER, + CARD_LABEL_VALUE_LIST_VALUE_FK INTEGER +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_ID_FK) REFERENCES LA_CARD_LABEL(CARD_LABEL_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_VALUE_CARD_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_VALUE_USER_FK) REFERENCES LA_USER(USER_ID); +-- +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT UNIQUE_LA_CARD_LABEL_VALUE_STRING UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT UNIQUE_LA_CARD_LABEL_VALUE_CARD_FK UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_CARD_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT UNIQUE_LA_CARD_LABEL_VALUE_USER_FK UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_USER_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT UNIQUE_LA_CARD_LABEL_VALUE_LIST_VALUE_FK UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT UNIQUE_LA_CARD_LABEL_VALUE_USE_UNIQUE_INDEX UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_USE_UNIQUE_INDEX); +-- + +DELIMITER // +CREATE FUNCTION LA_FUN_CARD_LABEL_VALUE_ENFORCE ( + CARD_LABEL_VALUE_TYPE ENUM('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST'), + CARD_LABEL_VALUE_STRING VARCHAR(32), + CARD_LABEL_VALUE_TIMESTAMP TIMESTAMP, + CARD_LABEL_VALUE_INT INTEGER, + CARD_LABEL_VALUE_CARD_FK INTEGER, + CARD_LABEL_VALUE_USER_FK INTEGER, + CARD_LABEL_VALUE_LIST_VALUE_FK INTEGER) + RETURNS BOOLEAN + DETERMINISTIC + BEGIN + RETURN (CARD_LABEL_VALUE_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST')) AND (CASE + WHEN CARD_LABEL_VALUE_TYPE = 'NULL' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'STRING' THEN + CARD_LABEL_VALUE_STRING IS NOT NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'TIMESTAMP' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NOT NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'INT' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NOT NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'CARD' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NOT NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'USER' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NOT NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'LIST' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NOT NULL + END); + END// + +CREATE TRIGGER CHECK_LA_CARD_LABEL_VALUE_INSERT BEFORE INSERT ON LA_CARD_LABEL_VALUE + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_CARD_LABEL_VALUE_ENFORCE(NEW.CARD_LABEL_VALUE_TYPE, NEW.CARD_LABEL_VALUE_STRING, NEW.CARD_LABEL_VALUE_TIMESTAMP, NEW.CARD_LABEL_VALUE_INT, NEW.CARD_LABEL_VALUE_CARD_FK, NEW.CARD_LABEL_VALUE_USER_FK, NEW.CARD_LABEL_VALUE_LIST_VALUE_FK)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + +CREATE TRIGGER CHECK_LA_CARD_LABEL_VALUE_UPDATE BEFORE UPDATE ON LA_CARD_LABEL_VALUE + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_CARD_LABEL_VALUE_ENFORCE(NEW.CARD_LABEL_VALUE_TYPE, NEW.CARD_LABEL_VALUE_STRING, NEW.CARD_LABEL_VALUE_TIMESTAMP, NEW.CARD_LABEL_VALUE_INT, NEW.CARD_LABEL_VALUE_CARD_FK, NEW.CARD_LABEL_VALUE_USER_FK, NEW.CARD_LABEL_VALUE_LIST_VALUE_FK)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// +DELIMITER ; + + +-- EVENT +CREATE TABLE LA_EVENT ( + EVENT_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + EVENT_CARD_ID_FK INTEGER NOT NULL, + EVENT_USER_ID_FK INTEGER NOT NULL, + EVENT_TYPE ENUM('LABEL_CREATE', 'LABEL_DELETE', 'CARD_MOVE', 'CARD_CREATE', 'CARD_ARCHIVE', 'CARD_BACKLOG', 'CARD_TRASH', 'CARD_UPDATE', 'ACTION_LIST_CREATE', 'ACTION_LIST_DELETE', 'ACTION_ITEM_CREATE', 'ACTION_ITEM_DELETE', 'ACTION_ITEM_MOVE', 'ACTION_ITEM_CHECK', 'ACTION_ITEM_UNCHECK', 'COMMENT_CREATE', 'COMMENT_UPDATE', 'COMMENT_DELETE', 'FILE_UPLOAD', 'FILE_DELETE', 'DESCRIPTION_CREATE', 'DESCRIPTION_UPDATE') NOT NULL, + EVENT_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + EVENT_CARD_DATA_ID_FK INTEGER, + EVENT_PREV_CARD_DATA_ID_FK INTEGER, + EVENT_NEW_CARD_DATA_ID_FK INTEGER, + EVENT_COLUMN_ID_FK INTEGER, + EVENT_PREV_COLUMN_ID_FK INTEGER, + EVENT_LABEL_NAME VARCHAR(32), + EVENT_LABEL_TYPE ENUM('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER') DEFAULT 'NULL' NOT NULL, + EVENT_VALUE_INT INTEGER, + EVENT_VALUE_STRING VARCHAR(255) NULL, + EVENT_VALUE_TIMESTAMP TIMESTAMP NULL DEFAULT NULL, + EVENT_VALUE_CARD_FK INTEGER NULL DEFAULT NULL, + EVENT_VALUE_USER_FK INTEGER NULL DEFAULT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +-- CONSTRAINTS; +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_PREV_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_PREV_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_NEW_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_VALUE_CARD_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_VALUE_USER_FK) REFERENCES LA_USER(USER_ID); + + +DELIMITER // +CREATE FUNCTION LA_FUN_EVENT_ENFORCE (EVENT_TYPE ENUM('LABEL_CREATE', 'LABEL_DELETE', 'CARD_MOVE', 'CARD_CREATE', 'CARD_ARCHIVE', 'CARD_BACKLOG', 'CARD_TRASH', 'CARD_UPDATE', 'ACTION_LIST_CREATE', 'ACTION_LIST_DELETE', 'ACTION_ITEM_CREATE', 'ACTION_ITEM_DELETE', 'ACTION_ITEM_MOVE', 'ACTION_ITEM_CHECK', 'ACTION_ITEM_UNCHECK', 'COMMENT_CREATE', 'COMMENT_UPDATE', 'COMMENT_DELETE', 'FILE_UPLOAD', 'FILE_DELETE', 'DESCRIPTION_CREATE', 'DESCRIPTION_UPDATE')) + RETURNS BOOLEAN + DETERMINISTIC + BEGIN + RETURN EVENT_TYPE IN ('LABEL_CREATE', 'LABEL_DELETE', 'CARD_MOVE', 'CARD_CREATE', 'CARD_ARCHIVE', 'CARD_BACKLOG', 'CARD_TRASH', 'CARD_UPDATE', 'ACTION_LIST_CREATE', 'ACTION_LIST_DELETE', 'ACTION_ITEM_CREATE', 'ACTION_ITEM_DELETE', 'ACTION_ITEM_MOVE', 'ACTION_ITEM_CHECK', 'ACTION_ITEM_UNCHECK', 'COMMENT_CREATE', 'COMMENT_UPDATE', 'COMMENT_DELETE', 'FILE_UPLOAD', 'FILE_DELETE', 'DESCRIPTION_CREATE', 'DESCRIPTION_UPDATE'); + END// + +DELIMITER // +CREATE FUNCTION LA_FUN_EVENT_LABEL_TYPE_ENFORCE (EVENT_LABEL_TYPE ENUM('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER')) + RETURNS BOOLEAN + DETERMINISTIC + BEGIN + RETURN EVENT_LABEL_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER'); + END// + +CREATE TRIGGER CHECK_LA_EVENT_INSERT BEFORE INSERT ON LA_EVENT + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_EVENT_ENFORCE(NEW.EVENT_TYPE)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + IF (NOT LA_FUN_EVENT_LABEL_TYPE_ENFORCE(NEW.EVENT_LABEL_TYPE)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + +CREATE TRIGGER CHECK_LA_EVENT_UPDATE BEFORE UPDATE ON LA_EVENT + FOR EACH ROW + BEGIN + IF (NOT LA_FUN_EVENT_ENFORCE(NEW.EVENT_TYPE)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + IF (NOT LA_FUN_EVENT_LABEL_TYPE_ENFORCE(NEW.EVENT_LABEL_TYPE)) THEN + CALL RAISE_CHECK_ERROR; + END IF; + END// + + +DELIMITER ; + + + +--ROLE+PERMISSION +CREATE TABLE LA_ROLE ( + ROLE_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + ROLE_NAME VARCHAR(32) NOT NULL, + ROLE_REMOVABLE BOOLEAN NOT NULL, + ROLE_HIDDEN BOOLEAN DEFAULT FALSE NOT NULL, + ROLE_READONLY BOOLEAN DEFAULT FALSE NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_ROLE ADD CONSTRAINT UNIQUE_LA_ROLE_ROLE_NAME UNIQUE(ROLE_NAME); + + +-- table role <-> permission +CREATE TABLE LA_ROLE_PERMISSION ( + ROLE_ID_FK INTEGER NOT NULL, + PERMISSION VARCHAR(64) NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_ROLE_PERMISSION ADD FOREIGN KEY(ROLE_ID_FK) REFERENCES LA_ROLE(ROLE_ID); +ALTER TABLE LA_ROLE_PERMISSION ADD CONSTRAINT UNIQUE_LA_ROLE_PERMISSION UNIQUE(ROLE_ID_FK, PERMISSION); + +-- junction table user <-> role for base role +CREATE TABLE LA_USER_ROLE ( + USER_ID_FK INTEGER NOT NULL, + ROLE_ID_FK INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_USER_ROLE ADD FOREIGN KEY(USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_USER_ROLE ADD FOREIGN KEY(ROLE_ID_FK) REFERENCES LA_ROLE(ROLE_ID); +ALTER TABLE LA_USER_ROLE ADD CONSTRAINT UNIQUE_LA_USER_ROLE UNIQUE(USER_ID_FK, ROLE_ID_FK); + + +-- PROJECTS and PROJECT ROLE HANDLING + +CREATE TABLE LA_PROJECT_ROLE ( + PROJECT_ROLE_ID INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL, + PROJECT_ROLE_NAME VARCHAR(32) NOT NULL, + PROJECT_ID_FK INTEGER NOT NULL, + PROJECT_ROLE_REMOVABLE BOOLEAN DEFAULT TRUE NOT NULL, + PROJECT_ROLE_HIDDEN BOOLEAN DEFAULT FALSE NOT NULL, + PROJECT_ROLE_READONLY BOOLEAN DEFAULT FALSE NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_PROJECT_ROLE ADD CONSTRAINT UNIQUE_LA_PROJECT_ROLE_NAME UNIQUE(PROJECT_ID_FK, PROJECT_ROLE_NAME); +ALTER TABLE LA_PROJECT_ROLE ADD FOREIGN KEY(PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); + +CREATE TABLE LA_PROJECT_ROLE_PERMISSION ( + PROJECT_ROLE_ID_FK INTEGER NOT NULL, + PERMISSION VARCHAR(64) NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_PROJECT_ROLE_PERMISSION ADD FOREIGN KEY(PROJECT_ROLE_ID_FK) REFERENCES LA_PROJECT_ROLE(PROJECT_ROLE_ID); +ALTER TABLE LA_PROJECT_ROLE_PERMISSION ADD CONSTRAINT UNIQUE_LA_PROJECT_ROLE_PERMISSION UNIQUE(PROJECT_ROLE_ID_FK, PERMISSION); + + +-- junction table project <-> user <-> role +CREATE TABLE LA_PROJECT_USER_ROLE ( + PROJECT_ID_FK INTEGER NOT NULL, + USER_ID_FK INTEGER NOT NULL, + PROJECT_ROLE_ID_FK INTEGER NOT NULL +) ENGINE=InnoDB CHARACTER SET=utf8 COLLATE utf8_bin; +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(PROJECT_ROLE_ID_FK) REFERENCES LA_PROJECT_ROLE(PROJECT_ROLE_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD CONSTRAINT UNIQUE_LA_PROJECT_USER_ROLE UNIQUE(PROJECT_ID_FK, USER_ID_FK, PROJECT_ROLE_ID_FK); + +-- + +-- TRIGGERS FOR TIME STATS +DELIMITER // +CREATE TRIGGER TRIG_CARD_TIME_STATS_INS AFTER INSERT ON LA_EVENT + FOR EACH ROW + BEGIN + UPDATE LA_CARD SET CARD_LAST_UPDATED = NEW.EVENT_TIME, CARD_LAST_UPDATED_USER_ID_FK = NEW.EVENT_USER_ID_FK WHERE CARD_ID = NEW.EVENT_CARD_ID_FK; + END// + +CREATE TRIGGER TRIG_CARD_TIME_STATS_DEL AFTER DELETE ON LA_EVENT + FOR EACH ROW + BEGIN + UPDATE LA_CARD SET CARD_LAST_UPDATED = NOW(), CARD_LAST_UPDATED_USER_ID_FK = OLD.EVENT_USER_ID_FK WHERE CARD_ID = OLD.EVENT_CARD_ID_FK; + END// + +DELIMITER ; + +-- VIEWS +CREATE VIEW LA_CARD_WITH_BOARD_ID AS ( + SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER, BOARD_COLUMN_BOARD_ID_FK "BOARD_ID", BOARD_COLUMN_LOCATION + FROM LA_CARD INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK); + +CREATE VIEW LA_CARD_DATA_FULL AS ( + SELECT CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_TYPE, CARD_DATA_CONTENT, CARD_DATA_ORDER, CARD_DATA_DELETED, EVENT_TIME, EVENT_TYPE, EVENT_PREV_CARD_DATA_ID_FK, EVENT_USER_ID_FK + FROM LA_CARD_DATA INNER JOIN LA_EVENT ON LA_CARD_DATA.CARD_DATA_ID = LA_EVENT.EVENT_CARD_DATA_ID_FK); + +CREATE VIEW LA_CARD_DATA_COUNT AS ( + SELECT BOARD_ID, CARD_ID, CARD_BOARD_COLUMN_ID_FK,CARD_DATA_TYPE, COUNT(CARD_DATA_TYPE) AS CARD_DATA_TYPE_COUNT FROM LA_CARD_WITH_BOARD_ID INNER JOIN LA_CARD_DATA ON LA_CARD_DATA.CARD_DATA_CARD_ID_FK = CARD_ID + WHERE CARD_DATA_DELETED = FALSE + GROUP BY BOARD_ID, CARD_ID, CARD_BOARD_COLUMN_ID_FK, CARD_DATA_TYPE +); + +CREATE VIEW LA_CARD_DATA_UPLOAD_CONTENT_LIGHT AS ( + SELECT EVENT_USER_ID_FK, EVENT_TIME, CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_CONTENT, CARD_DATA_DELETED, DISPLAYED_NAME, SIZE, CONTENT_TYPE + FROM LA_CARD_DATA INNER JOIN LA_CARD_DATA_UPLOAD ON LA_CARD_DATA.CARD_DATA_ID = LA_CARD_DATA_UPLOAD.CARD_DATA_ID_FK + INNER JOIN LA_CARD_DATA_UPLOAD_CONTENT ON LA_CARD_DATA_UPLOAD.CARD_DATA_UPLOAD_CONTENT_DIGEST_FK = LA_CARD_DATA_UPLOAD_CONTENT.DIGEST + INNER JOIN LA_EVENT ON EVENT_CARD_DATA_ID_FK = CARD_DATA_ID + WHERE CARD_DATA_TYPE = 'FILE' AND EVENT_TYPE = 'FILE_UPLOAD' AND CARD_DATA_DELETED = FALSE); + +CREATE VIEW LA_CARD_FULL AS ( + SELECT LA_CARD.CARD_ID AS CARD_ID, LA_CARD.CARD_NAME AS CARD_NAME, LA_CARD.CARD_SEQ_NUMBER AS CARD_SEQ_NUMBER, LA_CARD.CARD_BOARD_COLUMN_ID_FK AS CARD_BOARD_COLUMN_ID_FK, LA_CARD.CARD_ORDER AS CARD_ORDER, LA_EVENT.EVENT_TIME AS CREATE_TIME, LA_EVENT.EVENT_USER_ID_FK AS CREATE_USER, + LA_CARD.CARD_LAST_UPDATED AS LAST_UPDATE_TIME, LA_CARD.CARD_LAST_UPDATED_USER_ID_FK AS LAST_UPDATE_USER, PROJECT_SHORT_NAME, PROJECT_ID, BOARD_SHORT_NAME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_LOCATION FROM LA_CARD + INNER JOIN LA_EVENT ON LA_CARD.CARD_ID = LA_EVENT.EVENT_CARD_ID_FK AND LA_EVENT.EVENT_TYPE = 'CARD_CREATE' + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD ON LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID + INNER JOIN LA_PROJECT ON LA_PROJECT.PROJECT_ID = LA_BOARD.BOARD_PROJECT_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID); + +CREATE VIEW LA_ASSIGNED_CARD AS ( + SELECT LA_CARD.CARD_ID AS ASSIGNED_CARD_ID, LA_CARD.CARD_LAST_UPDATED AS ASSIGNED_EVENT_TIME, CARD_LABEL_VALUE_USER_FK AS ASSIGNED_USER_ID, BOARD_COLUMN_DEFINITION_VALUE AS ASSIGNED_CARD_STATUS FROM LA_CARD + INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID + INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID + WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'SYSTEM' AND LA_CARD_LABEL.CARD_LABEL_NAME = 'ASSIGNED' + AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE); + +CREATE VIEW LA_ASSIGNED_CARD_PROJECT AS ( + SELECT LA_CARD.CARD_ID AS ASSIGNED_CARD_ID, LA_CARD.CARD_LAST_UPDATED AS ASSIGNED_EVENT_TIME, CARD_LABEL_VALUE_USER_FK AS ASSIGNED_USER_ID, BOARD_COLUMN_DEFINITION_VALUE AS ASSIGNED_CARD_STATUS, LA_PROJECT.PROJECT_SHORT_NAME AS ASSIGNED_PROJECT_SHORT_NAME FROM LA_CARD + INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID + INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID + INNER JOIN LA_PROJECT ON LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_PROJECT_ID_FK = LA_PROJECT.PROJECT_ID + WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'SYSTEM' AND LA_CARD_LABEL.CARD_LABEL_NAME = 'ASSIGNED' + AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE); + +CREATE VIEW LA_BOARD_COLUMN_INFO AS ( + SELECT PROJECT_ID, PROJECT_NAME, BOARD_ID, BOARD_NAME, BOARD_SHORT_NAME, BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR + FROM LA_BOARD_COLUMN INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID + INNER JOIN LA_PROJECT ON BOARD_PROJECT_ID_FK = PROJECT_ID + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID = BOARD_COLUMN_DEFINITION_ID_FK +); + +CREATE VIEW LA_BOARD_COLUMN_FULL AS ( + SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR + FROM LA_BOARD_COLUMN INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID +); + +-- DEFAULT DATA +-- ANONYMOUS USER DEFAULT ROLES AND PERMISSION +INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('system', 'anonymous'); +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES('ANONYMOUS', FALSE, TRUE, TRUE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ANONYMOUS'), 'READ'); +-- ADMIN AND DEFAULT ROLES AND PERMISSION +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES('ADMIN', FALSE, FALSE, TRUE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'ADMINISTRATION'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'SEARCH'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_PROFILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'PROJECT_ADMINISTRATION'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_PROJECT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'RENAME_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'READ'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'TOGGLE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'ORDER_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MANAGE_LABEL_VALUE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_FILE'); + + +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE) VALUES('DEFAULT', FALSE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'READ'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'SEARCH'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_PROFILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MOVE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'TOGGLE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MOVE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'ORDER_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MANAGE_LABEL_VALUE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_CARD_COMMENT'); + +-- +INSERT INTO LA_PROJECT(PROJECT_NAME, PROJECT_SHORT_NAME) VALUES ('Default','DEFAULT'); +-- DEFAULT ANON ROLE +INSERT INTO LA_PROJECT_ROLE(PROJECT_ROLE_NAME, PROJECT_ID_FK, PROJECT_ROLE_HIDDEN, PROJECT_ROLE_READONLY, PROJECT_ROLE_REMOVABLE) VALUES('ANONYMOUS', (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, TRUE, FALSE); +INSERT INTO LA_PROJECT_ROLE_PERMISSION(PROJECT_ROLE_ID_FK, PERMISSION) VALUES (LAST_INSERT_ID(), 'READ'); +-- DEFAULT LABELS FOR Default PROJECT +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), FALSE, 'USER', 'SYSTEM', 'ASSIGNED', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, 'TIMESTAMP', 'SYSTEM', 'DUE_DATE', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, 'LIST', 'SYSTEM', 'MILESTONE', 0); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT')), 0, 'Unplanned'); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), FALSE, 'USER', 'SYSTEM', 'WATCHED_BY', 0); +--DEFAULT COLUMN DEFINITION FOR Default PROJECT +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'OPEN', 14242639); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'CLOSED', 6076508); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'BACKLOG', 4361162); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'DEFERRED', 15773006); + diff --git a/src/main/resources/io/lavagna/db/PGSQL/V1__INITIAL_VERSION.sql b/src/main/resources/io/lavagna/db/PGSQL/V1__INITIAL_VERSION.sql new file mode 100644 index 000000000..b3ff32856 --- /dev/null +++ b/src/main/resources/io/lavagna/db/PGSQL/V1__INITIAL_VERSION.sql @@ -0,0 +1,682 @@ +-- +-- This file is part of lavagna. +-- +-- lavagna 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. +-- +-- lavagna 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 lavagna. If not, see . +-- + +-- +create extension unaccent; +-- + + +-- CONFIGURATION TABLE +CREATE TABLE LA_CONF ( + CONF_KEY VARCHAR(64) PRIMARY KEY NOT NULL, + CONF_VALUE TEXT NOT NULL +); + +-- USER +CREATE TABLE LA_USER ( + USER_ID SERIAL PRIMARY KEY NOT NULL, + USER_PROVIDER VARCHAR(16) NOT NULL, + USER_NAME VARCHAR(128) NOT NULL, + USER_EMAIL VARCHAR(256), + USER_ENABLED BOOLEAN, + USER_DISPLAY_NAME VARCHAR(256), + USER_EMAIL_NOTIFICATION BOOLEAN DEFAULT TRUE NOT NULL, + USER_LAST_EMAIL_SENT TIMESTAMP DEFAULT NULL, + USER_LAST_CHECKPOINT_COUNT INTEGER DEFAULT 0 NOT NULL, + USER_LAST_CHECKED TIMESTAMP DEFAULT NULL, + USER_MEMBER_SINCE TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); +-- CONSTRAINTS +ALTER TABLE LA_USER ADD CONSTRAINT "UNIQUE_LA_USER_USER_NAME" UNIQUE(USER_PROVIDER, USER_NAME); + +-- USER REMEMBER +CREATE TABLE LA_USER_REMEMBER ( + USER_REMEMBER_HASHED_TOKEN CHAR(64) NOT NULL, + USER_REMEMBER_ID_FK INTEGER NOT NULL, + USER_REMEMBER_LAST_USE TIMESTAMP NOT NULL, + PRIMARY KEY(USER_REMEMBER_ID_FK, USER_REMEMBER_HASHED_TOKEN) +); +-- CONSTRAINTS +ALTER TABLE LA_USER_REMEMBER ADD FOREIGN KEY(USER_REMEMBER_ID_FK) REFERENCES LA_USER(USER_ID); + +-- PROJECT +CREATE TABLE LA_PROJECT ( + PROJECT_ID SERIAL PRIMARY KEY NOT NULL, + PROJECT_NAME VARCHAR(128) NOT NULL, + PROJECT_SHORT_NAME VARCHAR(8) NOT NULL, + PROJECT_ARCHIVED BOOLEAN DEFAULT FALSE NOT NULL, + PROJECT_DESCRIPTION TEXT +); +ALTER TABLE LA_PROJECT ADD CONSTRAINT "UNIQUE_LA_PROJECT_SHORT_NAME" UNIQUE(PROJECT_SHORT_NAME); + +-- BOARDS +CREATE TABLE LA_BOARD ( + BOARD_ID SERIAL PRIMARY KEY NOT NULL, + BOARD_PROJECT_ID_FK INTEGER NOT NULL, + BOARD_NAME VARCHAR(128) NOT NULL, + BOARD_SHORT_NAME VARCHAR(8) NOT NULL, + BOARD_ARCHIVED BOOLEAN DEFAULT FALSE NOT NULL, + BOARD_DESCRIPTION TEXT +); +-- CONSTRAINTS +ALTER TABLE LA_BOARD ADD CONSTRAINT "UNIQUE_LA_BOARD_SHORT_NAME" UNIQUE(BOARD_SHORT_NAME); +ALTER TABLE LA_BOARD ADD FOREIGN KEY(BOARD_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +-- INDEXES +CREATE INDEX "BOARD_PROJECT_ID_FK_IDX" ON LA_BOARD(BOARD_PROJECT_ID_FK); + +-- BOARD COUNTERS +CREATE TABLE LA_BOARD_COUNTER ( + BOARD_COUNTER_ID_FK INTEGER PRIMARY KEY NOT NULL, + BOARD_COUNTER_CARD_SEQUENCE INTEGER NOT NULL +); +ALTER TABLE LA_BOARD_COUNTER ADD FOREIGN KEY(BOARD_COUNTER_ID_FK) REFERENCES LA_BOARD(BOARD_ID); + +-- BOARD DEFINITION, FOR STATISTICS AND MICROMANAGEMENT +CREATE TABLE LA_BOARD_COLUMN_DEFINITION ( + BOARD_COLUMN_DEFINITION_ID SERIAL PRIMARY KEY NOT NULL, + BOARD_COLUMN_DEFINITION_PROJECT_ID_FK INTEGER NOT NULL, + BOARD_COLUMN_DEFINITION_VALUE VARCHAR(24) NOT NULL, + BOARD_COLUMN_DEFINITION_COLOR INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD FOREIGN KEY(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD CONSTRAINT "BOARD_COLUMN_DEFINITION_VALUE" CHECK(BOARD_COLUMN_DEFINITION_VALUE IN ('OPEN', 'CLOSED', 'BACKLOG', 'DEFERRED')); +ALTER TABLE LA_BOARD_COLUMN_DEFINITION ADD CONSTRAINT "BOARD_COLUMN_DEFINITION_COLOR_UNIQUE" UNIQUE(BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_COLOR); + + +-- BOARD COLUMN, FOR CATEGORIZING A CARD +CREATE TABLE LA_BOARD_COLUMN ( + BOARD_COLUMN_ID SERIAL PRIMARY KEY NOT NULL, + BOARD_COLUMN_NAME VARCHAR(128) NOT NULL, + BOARD_COLUMN_ORDER INTEGER NOT NULL, + BOARD_COLUMN_LOCATION VARCHAR(16) NOT NULL, + BOARD_COLUMN_BOARD_ID_FK INTEGER NOT NULL, + BOARD_COLUMN_DEFINITION_ID_FK INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_BOARD_COLUMN ADD FOREIGN KEY(BOARD_COLUMN_BOARD_ID_FK) REFERENCES LA_BOARD(BOARD_ID); +ALTER TABLE LA_BOARD_COLUMN ADD FOREIGN KEY(BOARD_COLUMN_DEFINITION_ID_FK) REFERENCES LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_ID); +ALTER TABLE LA_BOARD_COLUMN ADD CONSTRAINT "BOARD_COLUMN_LOCATION_VALUE" CHECK(BOARD_COLUMN_LOCATION IN ('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH')); +ALTER TABLE LA_BOARD_COLUMN ADD CONSTRAINT "BOARD_COLUMN_LOCATION_AND_NAME" CHECK( +CASE + WHEN BOARD_COLUMN_LOCATION = 'BOARD' AND BOARD_COLUMN_NAME IN ('BACKLOG', 'ARCHIVE', 'TRASH') THEN FALSE + ELSE TRUE +END); +-- INDEXES +CREATE INDEX "BOARD_COLUMN_BOARD_ID_FK_IDX" ON LA_BOARD_COLUMN(BOARD_COLUMN_BOARD_ID_FK); +CREATE INDEX "BOARD_COLUMN_DEFINITION_ID_FK_IDX" ON LA_BOARD_COLUMN(BOARD_COLUMN_DEFINITION_ID_FK); + +-- BOARD STATISTICS +CREATE TABLE LA_BOARD_STATISTICS ( + BOARD_STATISTICS_TIME TIMESTAMP NOT NULL, + BOARD_STATISTICS_BOARD_ID_FK INTEGER NOT NULL, + BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK INTEGER NOT NULL, + BOARD_STATISTICS_LOCATION VARCHAR(16) NOT NULL, + BOARD_STATISTICS_COUNT INTEGER NOT NULL +); +ALTER TABLE LA_BOARD_STATISTICS ADD FOREIGN KEY(BOARD_STATISTICS_BOARD_ID_FK) REFERENCES LA_BOARD(BOARD_ID); +ALTER TABLE LA_BOARD_STATISTICS ADD FOREIGN KEY(BOARD_STATISTICS_COLUMN_DEFINITION_ID_FK) REFERENCES LA_BOARD_COLUMN_DEFINITION(BOARD_COLUMN_DEFINITION_ID); +ALTER TABLE LA_BOARD_STATISTICS ADD CONSTRAINT "OARD_STATISTICS_LOCATION_VALUE" CHECK(BOARD_STATISTICS_LOCATION IN ('BOARD', 'BACKLOG', 'ARCHIVE', 'TRASH')); +-- INDEXES +CREATE INDEX "BOARD_STATISTICS_BOARD_ID_FK_IDX" ON LA_BOARD_STATISTICS(BOARD_STATISTICS_BOARD_ID_FK); + +-- LATEST BOARD STATISTICS BY DAY +CREATE VIEW LA_BOARD_STATISTICS_DAYS AS (SELECT MAX(BOARD_STATISTICS_TIME) AS DAY FROM LA_BOARD_STATISTICS GROUP BY CAST(BOARD_STATISTICS_TIME AS DATE)); + +-- CARD +CREATE TABLE LA_CARD ( + CARD_ID SERIAL PRIMARY KEY NOT NULL, + CARD_NAME VARCHAR(255) NOT NULL, + CARD_NAME_TSVECTOR TSVECTOR NOT NULL, + CARD_BOARD_COLUMN_ID_FK INTEGER NOT NULL, + CARD_ORDER INTEGER NOT NULL, + CARD_SEQ_NUMBER INTEGER NOT NULL, + CARD_USER_ID_FK INTEGER NOT NULL, + CARD_LAST_UPDATED TIMESTAMP NOT NULL, + CARD_LAST_UPDATED_USER_ID_FK INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_BOARD_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_CARD ADD FOREIGN KEY(CARD_LAST_UPDATED_USER_ID_FK) REFERENCES LA_USER(USER_ID); +-- INDEXES +CREATE INDEX "CARD_BOARD_COLUMN_ID_FK_IDX" ON LA_CARD(CARD_BOARD_COLUMN_ID_FK); +CREATE INDEX "CARD_USER_ID_FK_IDX" ON LA_CARD(CARD_USER_ID_FK); +CREATE INDEX "CARD_LAST_UPDATED_USER_ID_FK_IDX" ON LA_CARD(CARD_LAST_UPDATED_USER_ID_FK); +-- TRIGGER: + +CREATE FUNCTION la_card_vector_update() RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.CARD_NAME_TSVECTOR = to_tsvector('pg_catalog.english', unaccent(NEW.CARD_NAME)); + END IF; + IF TG_OP = 'UPDATE' THEN + IF NEW.CARD_NAME <> OLD.CARD_NAME THEN + NEW.CARD_NAME_TSVECTOR = to_tsvector('pg_catalog.english', unaccent(NEW.CARD_NAME)); + END IF; + END IF; + RETURN NEW; +END +$$ LANGUAGE 'plpgsql'; + +CREATE TRIGGER LA_CARD_NAME_INSERT_OR_UPDATE BEFORE INSERT OR UPDATE ON LA_CARD + FOR EACH ROW EXECUTE PROCEDURE la_card_vector_update(); +-- INDEX: +CREATE INDEX LA_CARD_NAME_TSVECTOR_IDX ON LA_CARD USING GIN(CARD_NAME_TSVECTOR); + +CREATE TABLE LA_CARD_DATA ( + CARD_DATA_ID SERIAL PRIMARY KEY NOT NULL, + CARD_DATA_CARD_ID_FK INTEGER NOT NULL, + CARD_DATA_REFERENCE_ID INTEGER, + CARD_DATA_DELETED BOOLEAN DEFAULT FALSE NOT NULL, + CARD_DATA_TYPE VARCHAR(128) NOT NULL, + CARD_DATA_CONTENT TEXT NOT NULL, + CARD_DATA_CONTENT_TSVECTOR TSVECTOR NOT NULL, + CARD_DATA_ORDER INTEGER NOT NULL +); +-- CONSTRAINTS: +ALTER TABLE LA_CARD_DATA ADD FOREIGN KEY(CARD_DATA_CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_DATA ADD FOREIGN KEY(CARD_DATA_REFERENCE_ID) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_CARD_DATA ADD CONSTRAINT "CARD_DATA_TYPE_VALUE" CHECK(CARD_DATA_TYPE IN ('COMMENT', 'ACTION_LIST', 'ACTION_CHECKED', 'ACTION_UNCHECKED', 'FILE', 'COMMENT_HISTORY', 'DESCRIPTION', 'DESCRIPTION_HISTORY')); +ALTER TABLE LA_CARD_DATA ADD CONSTRAINT "CARD_DATA_ENSURE_REFERENCE_ID" CHECK( +CASE + WHEN CARD_DATA_TYPE IN ('COMMENT','ACTION_LIST', 'DESCRIPTION') THEN CARD_DATA_REFERENCE_ID IS NULL + WHEN CARD_DATA_TYPE IN ('ACTION_CHECKED', 'ACTION_UNCHECKED', 'COMMENT_HISTORY', 'DESCRIPTION_HISTORY') THEN CARD_DATA_REFERENCE_ID IS NOT NULL + ELSE TRUE +END); +-- INDEXES +CREATE INDEX "CARD_DATA_CARD_ID_FK_IDX" ON LA_CARD_DATA(CARD_DATA_CARD_ID_FK); +CREATE INDEX "CARD_DATA_REFERENCE_ID_IDX" ON LA_CARD_DATA(CARD_DATA_REFERENCE_ID); +-- TRIGGER: + +CREATE FUNCTION la_card_data_vector_update() RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.CARD_DATA_CONTENT_TSVECTOR = to_tsvector('pg_catalog.english', unaccent(NEW.CARD_DATA_CONTENT)); + END IF; + IF TG_OP = 'UPDATE' THEN + IF NEW.CARD_DATA_CONTENT <> OLD.CARD_DATA_CONTENT THEN + NEW.CARD_DATA_CONTENT_TSVECTOR = to_tsvector('pg_catalog.english', unaccent(NEW.CARD_DATA_CONTENT)); + END IF; + END IF; + RETURN NEW; +END +$$ LANGUAGE 'plpgsql'; + + +CREATE TRIGGER LA_CARD_DATA_INSERT_OR_UPDATE BEFORE INSERT OR UPDATE ON LA_CARD_DATA + FOR EACH ROW EXECUTE PROCEDURE la_card_data_vector_update(); +-- INDEX: +CREATE INDEX LA_CARD_DATA_CONTENT_TSVECTOR_IDX ON LA_CARD_DATA USING GIN(CARD_DATA_CONTENT_TSVECTOR); + +-- DATA UPLOAD +CREATE TABLE LA_CARD_DATA_UPLOAD_CONTENT ( + DIGEST CHAR(64) NOT NULL, + SIZE INTEGER NOT NULL, + CONTENT BYTEA NOT NULL, + CONTENT_TYPE VARCHAR(255) NOT NULL +); +ALTER TABLE LA_CARD_DATA_UPLOAD_CONTENT ADD CONSTRAINT "UNIQUE_LA_CARD_UPLOAD_CONTENT" UNIQUE(DIGEST); + +CREATE TABLE LA_CARD_DATA_UPLOAD ( + CARD_DATA_ID_FK INTEGER NOT NULL, + CARD_DATA_UPLOAD_CONTENT_DIGEST_FK CHAR(64) NOT NULL, + ORIGINAL_NAME VARCHAR(255) NOT NULL, + DISPLAYED_NAME VARCHAR(255) NOT NULL +); +ALTER TABLE LA_CARD_DATA_UPLOAD ADD FOREIGN KEY(CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_CARD_DATA_UPLOAD ADD FOREIGN KEY(CARD_DATA_UPLOAD_CONTENT_DIGEST_FK) REFERENCES LA_CARD_DATA_UPLOAD_CONTENT(DIGEST); +-- INDEXES +CREATE INDEX "CARD_DATA_UPLOAD_CONTENT_DIGEST_FK_IDX" ON LA_CARD_DATA_UPLOAD(CARD_DATA_UPLOAD_CONTENT_DIGEST_FK); + +-- CARD LABEL +CREATE TABLE LA_CARD_LABEL ( + CARD_LABEL_ID SERIAL PRIMARY KEY NOT NULL, + CARD_LABEL_PROJECT_ID_FK INTEGER NOT NULL, + CARD_LABEL_UNIQUE BOOLEAN DEFAULT FALSE NOT NULL, + CARD_LABEL_TYPE VARCHAR(16) NOT NULL, + CARD_LABEL_DOMAIN VARCHAR(16) NOT NULL, + CARD_LABEL_NAME VARCHAR(32) NOT NULL, + CARD_LABEL_COLOR INTEGER +); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT "CARD_LABEL_TYPE_VALUE" CHECK(CARD_LABEL_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST')); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT "CARD_LABEL_DOMAIN_VALUE" CHECK(CARD_LABEL_DOMAIN IN ('SYSTEM', 'USER')); +ALTER TABLE LA_CARD_LABEL ADD FOREIGN KEY(CARD_LABEL_PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_CARD_LABEL ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_PROJECT_ID_FK_NAME" UNIQUE(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_NAME); +-- INDEXES +CREATE INDEX "CARD_LABEL_PROJECT_ID_FK_IDX" ON LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK); + +-- junction table label <-> list values +CREATE TABLE LA_CARD_LABEL_LIST_VALUE ( + CARD_LABEL_LIST_VALUE_ID SERIAL PRIMARY KEY NOT NULL, + CARD_LABEL_ID_FK INTEGER NOT NULL, + CARD_LABEL_LIST_VALUE_ORDER INTEGER NOT NULL, + CARD_LABEL_LIST_VALUE VARCHAR(255) +); +ALTER TABLE LA_CARD_LABEL_LIST_VALUE ADD FOREIGN KEY(CARD_LABEL_ID_FK) REFERENCES LA_CARD_LABEL(CARD_LABEL_ID); +-- INDEXES +CREATE INDEX "LA_CARD_LABEL_LIST_VALUE_CARD_LABEL_ID_FK_IDX" ON LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK); +-- + +-- junction table label <-> card and card value +CREATE TABLE LA_CARD_LABEL_VALUE ( + CARD_LABEL_VALUE_ID SERIAL PRIMARY KEY NOT NULL, + CARD_LABEL_VALUE_USE_UNIQUE_INDEX BOOLEAN DEFAULT NULL, + CARD_LABEL_VALUE_DELETED BOOLEAN DEFAULT FALSE NOT NULL, + CARD_ID_FK INTEGER NOT NULL, + CARD_LABEL_ID_FK INTEGER NOT NULL, + CARD_LABEL_VALUE_TYPE VARCHAR(16) NOT NULL, + CARD_LABEL_VALUE_STRING VARCHAR(32), + CARD_LABEL_VALUE_TIMESTAMP TIMESTAMP, + CARD_LABEL_VALUE_INT INTEGER, + CARD_LABEL_VALUE_CARD_FK INTEGER, + CARD_LABEL_VALUE_USER_FK INTEGER, + CARD_LABEL_VALUE_LIST_VALUE_FK INTEGER +); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_ID_FK) REFERENCES LA_CARD_LABEL(CARD_LABEL_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_VALUE_CARD_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD FOREIGN KEY(CARD_LABEL_VALUE_USER_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "CARD_LABEL_VALUE_TYPE_VALUE" CHECK(CARD_LABEL_VALUE_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER', 'LIST')); +-- +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_STRING" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_STRING); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_CARD_FK" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_CARD_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_USER_FK" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_USER_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_LIST_VALUE_FK" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_TYPE, CARD_LABEL_VALUE_LIST_VALUE_FK); +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "UNIQUE_LA_CARD_LABEL_VALUE_USE_UNIQUE_INDEX" UNIQUE(CARD_ID_FK, CARD_LABEL_ID_FK, CARD_LABEL_VALUE_USE_UNIQUE_INDEX); +-- + +ALTER TABLE LA_CARD_LABEL_VALUE ADD CONSTRAINT "CARD_LABEL_VALUE_ENSURE_TYPE" CHECK( +CASE + WHEN CARD_LABEL_VALUE_TYPE = 'NULL' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'STRING' THEN + CARD_LABEL_VALUE_STRING IS NOT NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'TIMESTAMP' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NOT NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'INT' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NOT NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'CARD' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NOT NULL AND + CARD_LABEL_VALUE_USER_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'USER' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_USER_FK IS NOT NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NULL + WHEN CARD_LABEL_VALUE_TYPE = 'LIST' THEN + CARD_LABEL_VALUE_STRING IS NULL AND + CARD_LABEL_VALUE_TIMESTAMP IS NULL AND + CARD_LABEL_VALUE_INT IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_CARD_FK IS NULL AND + CARD_LABEL_VALUE_LIST_VALUE_FK IS NOT NULL +END); +-- INDEXES +CREATE INDEX "LA_CARD_LABEL_VALUE_CARD_ID_FK_IDX" ON LA_CARD_LABEL_VALUE(CARD_ID_FK); +CREATE INDEX "LA_CARD_LABEL_VALUE_CARD_LABEL_ID_FK_IDX" ON LA_CARD_LABEL_VALUE(CARD_LABEL_ID_FK); + +-- EVENT +CREATE TABLE LA_EVENT ( + EVENT_ID SERIAL PRIMARY KEY NOT NULL, + EVENT_CARD_ID_FK INTEGER NOT NULL, + EVENT_USER_ID_FK INTEGER NOT NULL, + EVENT_TYPE VARCHAR(32) NOT NULL, + EVENT_TIME TIMESTAMP NOT NULL, + EVENT_CARD_DATA_ID_FK INTEGER, + EVENT_PREV_CARD_DATA_ID_FK INTEGER, + EVENT_NEW_CARD_DATA_ID_FK INTEGER, + EVENT_COLUMN_ID_FK INTEGER, + EVENT_PREV_COLUMN_ID_FK INTEGER, + EVENT_LABEL_NAME VARCHAR(32), + EVENT_LABEL_TYPE VARCHAR(16) DEFAULT 'NULL' NOT NULL, + EVENT_VALUE_INT INTEGER, + EVENT_VALUE_STRING VARCHAR(255) NULL, + EVENT_VALUE_TIMESTAMP TIMESTAMP NULL, + EVENT_VALUE_CARD_FK INTEGER NULL, + EVENT_VALUE_USER_FK INTEGER NULL +); +-- CONSTRAINTS; +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_CARD_ID_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_EVENT ADD CONSTRAINT "EVENT_TYPE_VALUE" CHECK(EVENT_TYPE IN ('LABEL_CREATE', 'LABEL_DELETE', 'CARD_MOVE', 'CARD_CREATE', 'CARD_ARCHIVE', 'CARD_BACKLOG', 'CARD_TRASH', 'CARD_UPDATE', 'ACTION_LIST_CREATE', 'ACTION_LIST_DELETE', 'ACTION_ITEM_CREATE', 'ACTION_ITEM_DELETE', 'ACTION_ITEM_MOVE', 'ACTION_ITEM_CHECK', 'ACTION_ITEM_UNCHECK', 'COMMENT_CREATE', 'COMMENT_UPDATE', 'COMMENT_DELETE', 'FILE_UPLOAD', 'FILE_DELETE', 'DESCRIPTION_CREATE', 'DESCRIPTION_UPDATE')); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_PREV_COLUMN_ID_FK) REFERENCES LA_BOARD_COLUMN(BOARD_COLUMN_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_PREV_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_NEW_CARD_DATA_ID_FK) REFERENCES LA_CARD_DATA(CARD_DATA_ID); +ALTER TABLE LA_EVENT ADD CONSTRAINT "EVENT_LABEL_TYPE_VALUE" CHECK(EVENT_LABEL_TYPE IN ('NULL', 'STRING', 'TIMESTAMP', 'INT', 'CARD', 'USER')); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_VALUE_CARD_FK) REFERENCES LA_CARD(CARD_ID); +ALTER TABLE LA_EVENT ADD FOREIGN KEY(EVENT_VALUE_USER_FK) REFERENCES LA_USER(USER_ID); + + +--ROLE+PERMISSION +CREATE TABLE LA_ROLE ( + ROLE_ID SERIAL PRIMARY KEY NOT NULL, + ROLE_NAME VARCHAR(32) NOT NULL, + ROLE_REMOVABLE BOOLEAN NOT NULL, + ROLE_HIDDEN BOOLEAN DEFAULT FALSE NOT NULL, + ROLE_READONLY BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE LA_ROLE ADD CONSTRAINT "UNIQUE_LA_ROLE_ROLE_NAME" UNIQUE(ROLE_NAME); + + +-- table role <-> permission +CREATE TABLE LA_ROLE_PERMISSION ( + ROLE_ID_FK INTEGER NOT NULL, + PERMISSION VARCHAR(64) NOT NULL +); +ALTER TABLE LA_ROLE_PERMISSION ADD FOREIGN KEY(ROLE_ID_FK) REFERENCES LA_ROLE(ROLE_ID); +ALTER TABLE LA_ROLE_PERMISSION ADD CONSTRAINT "UNIQUE_LA_ROLE_PERMISSION" UNIQUE(ROLE_ID_FK, PERMISSION); + +-- junction table user <-> role for base role +CREATE TABLE LA_USER_ROLE ( + USER_ID_FK INTEGER NOT NULL, + ROLE_ID_FK INTEGER NOT NULL +); +ALTER TABLE LA_USER_ROLE ADD FOREIGN KEY(USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_USER_ROLE ADD FOREIGN KEY(ROLE_ID_FK) REFERENCES LA_ROLE(ROLE_ID); +ALTER TABLE LA_USER_ROLE ADD CONSTRAINT "UNIQUE_LA_USER_ROLE" UNIQUE(USER_ID_FK, ROLE_ID_FK); + + +-- PROJECTS and PROJECT ROLE HANDLING + +CREATE TABLE LA_PROJECT_ROLE ( + PROJECT_ROLE_ID SERIAL PRIMARY KEY NOT NULL, + PROJECT_ROLE_NAME VARCHAR(32) NOT NULL, + PROJECT_ID_FK INTEGER NOT NULL, + PROJECT_ROLE_REMOVABLE BOOLEAN DEFAULT TRUE NOT NULL, + PROJECT_ROLE_HIDDEN BOOLEAN DEFAULT FALSE NOT NULL, + PROJECT_ROLE_READONLY BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE LA_PROJECT_ROLE ADD CONSTRAINT "UNIQUE_LA_PROJECT_ROLE_NAME" UNIQUE(PROJECT_ID_FK, PROJECT_ROLE_NAME); +ALTER TABLE LA_PROJECT_ROLE ADD FOREIGN KEY(PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); + +CREATE TABLE LA_PROJECT_ROLE_PERMISSION ( + PROJECT_ROLE_ID_FK INTEGER NOT NULL, + PERMISSION VARCHAR(64) NOT NULL +); +ALTER TABLE LA_PROJECT_ROLE_PERMISSION ADD FOREIGN KEY(PROJECT_ROLE_ID_FK) REFERENCES LA_PROJECT_ROLE(PROJECT_ROLE_ID); +ALTER TABLE LA_PROJECT_ROLE_PERMISSION ADD CONSTRAINT "UNIQUE_LA_PROJECT_ROLE_PERMISSION" UNIQUE(PROJECT_ROLE_ID_FK, PERMISSION); + + +-- junction table project <-> user <-> role +CREATE TABLE LA_PROJECT_USER_ROLE ( + PROJECT_ID_FK INTEGER NOT NULL, + USER_ID_FK INTEGER NOT NULL, + PROJECT_ROLE_ID_FK INTEGER NOT NULL +); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(PROJECT_ID_FK) REFERENCES LA_PROJECT(PROJECT_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(USER_ID_FK) REFERENCES LA_USER(USER_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD FOREIGN KEY(PROJECT_ROLE_ID_FK) REFERENCES LA_PROJECT_ROLE(PROJECT_ROLE_ID); +ALTER TABLE LA_PROJECT_USER_ROLE ADD CONSTRAINT "UNIQUE_LA_PROJECT_USER_ROLE" UNIQUE(PROJECT_ID_FK, USER_ID_FK, PROJECT_ROLE_ID_FK); + +-- + +-- TRIGGERS FOR TIME STATS +CREATE FUNCTION la_card_update_last_updated() RETURNS trigger AS $$ + BEGIN + UPDATE LA_CARD SET CARD_LAST_UPDATED = NEW.EVENT_TIME, CARD_LAST_UPDATED_USER_ID_FK = NEW.EVENT_USER_ID_FK WHERE CARD_ID = NEW.EVENT_CARD_ID_FK; + RETURN NULL; + END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION la_card_update_last_deleted() RETURNS trigger AS $$ + BEGIN + UPDATE LA_CARD SET CARD_LAST_UPDATED = NOW(), CARD_LAST_UPDATED_USER_ID_FK = OLD.EVENT_USER_ID_FK WHERE CARD_ID = OLD.EVENT_CARD_ID_FK; + RETURN NULL; + END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER TRIG_CARD_TIME_STATS_INS AFTER INSERT ON LA_EVENT + FOR EACH ROW EXECUTE PROCEDURE la_card_update_last_updated(); + +CREATE TRIGGER TRIG_CARD_TIME_STATS_DEL AFTER DELETE ON LA_EVENT + FOR EACH ROW EXECUTE PROCEDURE la_card_update_last_deleted(); + +-- VIEWS +CREATE VIEW LA_CARD_WITH_BOARD_ID AS ( + SELECT CARD_ID, CARD_NAME, CARD_BOARD_COLUMN_ID_FK, CARD_ORDER, CARD_USER_ID_FK, CARD_SEQ_NUMBER, BOARD_COLUMN_BOARD_ID_FK AS BOARD_ID, BOARD_COLUMN_LOCATION + FROM LA_CARD INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK); + +CREATE VIEW LA_CARD_DATA_FULL AS ( + SELECT CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_TYPE, CARD_DATA_CONTENT, CARD_DATA_ORDER, CARD_DATA_DELETED, EVENT_TIME, EVENT_TYPE, EVENT_PREV_CARD_DATA_ID_FK, EVENT_USER_ID_FK + FROM LA_CARD_DATA INNER JOIN LA_EVENT ON LA_CARD_DATA.CARD_DATA_ID = LA_EVENT.EVENT_CARD_DATA_ID_FK); + +CREATE VIEW LA_CARD_DATA_COUNT AS ( + SELECT BOARD_ID, CARD_ID, CARD_BOARD_COLUMN_ID_FK,CARD_DATA_TYPE, COUNT(CARD_DATA_TYPE) AS CARD_DATA_TYPE_COUNT FROM LA_CARD_WITH_BOARD_ID INNER JOIN LA_CARD_DATA ON LA_CARD_DATA.CARD_DATA_CARD_ID_FK = CARD_ID + WHERE CARD_DATA_DELETED = FALSE + GROUP BY BOARD_ID, CARD_ID, CARD_BOARD_COLUMN_ID_FK, CARD_DATA_TYPE +); + + +CREATE VIEW LA_CARD_DATA_UPLOAD_CONTENT_LIGHT AS ( + SELECT EVENT_USER_ID_FK, EVENT_TIME, CARD_DATA_ID, CARD_DATA_CARD_ID_FK, CARD_DATA_REFERENCE_ID, CARD_DATA_CONTENT, CARD_DATA_DELETED, DISPLAYED_NAME, SIZE, CONTENT_TYPE + FROM LA_CARD_DATA INNER JOIN LA_CARD_DATA_UPLOAD ON LA_CARD_DATA.CARD_DATA_ID = LA_CARD_DATA_UPLOAD.CARD_DATA_ID_FK + INNER JOIN LA_CARD_DATA_UPLOAD_CONTENT ON LA_CARD_DATA_UPLOAD.CARD_DATA_UPLOAD_CONTENT_DIGEST_FK = LA_CARD_DATA_UPLOAD_CONTENT.DIGEST + INNER JOIN LA_EVENT ON EVENT_CARD_DATA_ID_FK = CARD_DATA_ID + WHERE CARD_DATA_TYPE = 'FILE' AND EVENT_TYPE = 'FILE_UPLOAD' AND CARD_DATA_DELETED = FALSE); + +CREATE VIEW LA_CARD_FULL AS ( + SELECT LA_CARD.CARD_ID AS CARD_ID, LA_CARD.CARD_NAME AS CARD_NAME, LA_CARD.CARD_SEQ_NUMBER AS CARD_SEQ_NUMBER, LA_CARD.CARD_BOARD_COLUMN_ID_FK AS CARD_BOARD_COLUMN_ID_FK, LA_CARD.CARD_ORDER AS CARD_ORDER, LA_EVENT.EVENT_TIME AS CREATE_TIME, LA_EVENT.EVENT_USER_ID_FK AS CREATE_USER, + LA_CARD.CARD_LAST_UPDATED AS LAST_UPDATE_TIME, LA_CARD.CARD_LAST_UPDATED_USER_ID_FK AS LAST_UPDATE_USER, PROJECT_SHORT_NAME, PROJECT_ID, BOARD_SHORT_NAME, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_LOCATION FROM LA_CARD + INNER JOIN LA_EVENT ON LA_CARD.CARD_ID = LA_EVENT.EVENT_CARD_ID_FK AND LA_EVENT.EVENT_TYPE = 'CARD_CREATE' + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD ON LA_BOARD_COLUMN.BOARD_COLUMN_BOARD_ID_FK = LA_BOARD.BOARD_ID + INNER JOIN LA_PROJECT ON LA_PROJECT.PROJECT_ID = LA_BOARD.BOARD_PROJECT_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID); + +CREATE VIEW LA_ASSIGNED_CARD AS ( + SELECT LA_CARD.CARD_ID AS ASSIGNED_CARD_ID, LA_CARD.CARD_LAST_UPDATED AS ASSIGNED_EVENT_TIME, CARD_LABEL_VALUE_USER_FK AS ASSIGNED_USER_ID, BOARD_COLUMN_DEFINITION_VALUE AS ASSIGNED_CARD_STATUS FROM LA_CARD + INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID + INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID + WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'SYSTEM' AND LA_CARD_LABEL.CARD_LABEL_NAME = 'ASSIGNED' + AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE +); + +CREATE VIEW LA_ASSIGNED_CARD_PROJECT AS ( + SELECT LA_CARD.CARD_ID AS ASSIGNED_CARD_ID, LA_CARD.CARD_LAST_UPDATED AS ASSIGNED_EVENT_TIME, CARD_LABEL_VALUE_USER_FK AS ASSIGNED_USER_ID, BOARD_COLUMN_DEFINITION_VALUE AS ASSIGNED_CARD_STATUS, LA_PROJECT.PROJECT_SHORT_NAME AS ASSIGNED_PROJECT_SHORT_NAME FROM LA_CARD + INNER JOIN LA_CARD_LABEL_VALUE ON LA_CARD_LABEL_VALUE.CARD_ID_FK = LA_CARD.CARD_ID + INNER JOIN LA_CARD_LABEL ON LA_CARD_LABEL.CARD_LABEL_ID = LA_CARD_LABEL_VALUE.CARD_LABEL_ID_FK + INNER JOIN LA_BOARD_COLUMN ON LA_BOARD_COLUMN.BOARD_COLUMN_ID = LA_CARD.CARD_BOARD_COLUMN_ID_FK + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON LA_BOARD_COLUMN.BOARD_COLUMN_DEFINITION_ID_FK = LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_ID + INNER JOIN LA_PROJECT ON LA_BOARD_COLUMN_DEFINITION.BOARD_COLUMN_DEFINITION_PROJECT_ID_FK = LA_PROJECT.PROJECT_ID + WHERE LA_CARD_LABEL.CARD_LABEL_DOMAIN = 'SYSTEM' AND LA_CARD_LABEL.CARD_LABEL_NAME = 'ASSIGNED' + AND LA_CARD_LABEL_VALUE.CARD_LABEL_VALUE_DELETED = FALSE +); + +CREATE VIEW LA_BOARD_COLUMN_INFO AS ( + SELECT PROJECT_ID, PROJECT_NAME, BOARD_ID, BOARD_NAME, BOARD_SHORT_NAME, BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_LOCATION, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR + FROM LA_BOARD_COLUMN INNER JOIN LA_BOARD ON BOARD_COLUMN_BOARD_ID_FK = BOARD_ID + INNER JOIN LA_PROJECT ON BOARD_PROJECT_ID_FK = PROJECT_ID + INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID = BOARD_COLUMN_DEFINITION_ID_FK +); + +CREATE VIEW LA_BOARD_COLUMN_FULL AS ( + SELECT BOARD_COLUMN_ID, BOARD_COLUMN_NAME, BOARD_COLUMN_ORDER, BOARD_COLUMN_LOCATION, BOARD_COLUMN_BOARD_ID_FK, BOARD_COLUMN_DEFINITION_ID, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR + FROM LA_BOARD_COLUMN INNER JOIN LA_BOARD_COLUMN_DEFINITION ON BOARD_COLUMN_DEFINITION_ID_FK = BOARD_COLUMN_DEFINITION_ID +); + + + +-- DEFAULT DATA +-- ANONYMOUS USER DEFAULT ROLES AND PERMISSION +INSERT INTO LA_USER(USER_PROVIDER, USER_NAME) VALUES ('system', 'anonymous'); +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES('ANONYMOUS', FALSE, TRUE, TRUE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ANONYMOUS'), 'READ'); +-- ADMIN AND DEFAULT ROLES AND PERMISSION +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE, ROLE_HIDDEN, ROLE_READONLY) VALUES('ADMIN', FALSE, FALSE, TRUE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'ADMINISTRATION'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'SEARCH'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_PROFILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'PROJECT_ADMINISTRATION'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_PROJECT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_BOARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'RENAME_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_COLUMN'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'READ'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_CARD_COMMENT'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'TOGGLE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MOVE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'ORDER_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'MANAGE_LABEL_VALUE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'CREATE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'DELETE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'ADMIN'), 'UPDATE_FILE'); + + +INSERT INTO LA_ROLE(ROLE_NAME, ROLE_REMOVABLE) VALUES('DEFAULT', FALSE); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'READ'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'SEARCH'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_PROFILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MOVE_CARD'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'TOGGLE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MOVE_ACTION_LIST_ITEM'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'ORDER_ACTION_LIST'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_FILE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'UPDATE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'DELETE_LABEL'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'MANAGE_LABEL_VALUE'); +INSERT INTO LA_ROLE_PERMISSION(ROLE_ID_FK, PERMISSION) VALUES + ((SELECT ROLE_ID FROM LA_ROLE WHERE ROLE_NAME = 'DEFAULT'), 'CREATE_CARD_COMMENT'); + +-- DEFAULT PROJECT +-- +INSERT INTO LA_PROJECT(PROJECT_NAME, PROJECT_SHORT_NAME) VALUES ('Default','DEFAULT'); +-- DEFAULT ANON ROLE +INSERT INTO LA_PROJECT_ROLE(PROJECT_ROLE_NAME, PROJECT_ID_FK, PROJECT_ROLE_HIDDEN, PROJECT_ROLE_READONLY, PROJECT_ROLE_REMOVABLE) VALUES('ANONYMOUS', (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, TRUE, FALSE); +INSERT INTO LA_PROJECT_ROLE_PERMISSION(PROJECT_ROLE_ID_FK, PERMISSION) VALUES (LASTVAL(), 'READ'); +-- DEFAULT LABELS FOR Default PROJECT +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), FALSE, 'USER', 'SYSTEM', 'ASSIGNED', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, 'TIMESTAMP', 'SYSTEM', 'DUE_DATE', 0); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), TRUE, 'LIST', 'SYSTEM', 'MILESTONE', 0); +INSERT INTO LA_CARD_LABEL_LIST_VALUE(CARD_LABEL_ID_FK, CARD_LABEL_LIST_VALUE_ORDER,CARD_LABEL_LIST_VALUE) VALUES ((SELECT CARD_LABEL_ID FROM LA_CARD_LABEL WHERE CARD_LABEL_NAME = 'MILESTONE' AND CARD_LABEL_PROJECT_ID_FK = (SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT')), 0, 'Unplanned'); +INSERT INTO LA_CARD_LABEL(CARD_LABEL_PROJECT_ID_FK, CARD_LABEL_UNIQUE, CARD_LABEL_TYPE, CARD_LABEL_DOMAIN, CARD_LABEL_NAME, CARD_LABEL_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), FALSE, 'USER', 'SYSTEM', 'WATCHED_BY', 0); +--DEFAULT COLUMN DEFINITION FOR Default PROJECT +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'OPEN', 14242639); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'CLOSED', 6076508); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'BACKLOG', 4361162); +INSERT INTO LA_BOARD_COLUMN_DEFINITION (BOARD_COLUMN_DEFINITION_PROJECT_ID_FK, BOARD_COLUMN_DEFINITION_VALUE, BOARD_COLUMN_DEFINITION_COLOR) VALUES ((SELECT PROJECT_ID FROM LA_PROJECT WHERE PROJECT_SHORT_NAME = 'DEFAULT'), 'DEFERRED', 15773006); diff --git a/src/main/resources/io/lavagna/i18n/messages_en.properties b/src/main/resources/io/lavagna/i18n/messages_en.properties new file mode 100644 index 000000000..346e66e18 --- /dev/null +++ b/src/main/resources/io/lavagna/i18n/messages_en.properties @@ -0,0 +1,697 @@ +# /404.html + +404.parentResource=Maybe the parent resource is still available... + +# generic crap +button.add=Add +button.delete=Delete +button.update=Update +button.preview=Preview +button.closePreview=Close Preview +button.cancel=Cancel +button.yes=Yes +button.no=No +button.save=Save +button.clear=Clear +button.reset=Reset +button.close=Close +button.upload=Upload +button.fileSelect=Select File +button.filesSelect=Select Files +button.next=Next +button.previous=Previous + +common.no-files-selected=No files selected +common.no-description-available=No description available +common.yes=Yes +common.no=No +common.edit=Edit +common.archive=Archive +common.unarchive=Remove from archive + +common.column.definition.OPEN=Open +common.column.definition.CLOSED=Closed +common.column.definition.BACKLOG=Backlog +common.column.definition.DEFERRED=Deferred + +# /index.html +index.lavagna=Lavagna +index.navbar.manageAccount=Manage account +index.navbar.administration=Administration +index.navbar.logout=Logout +index.navbar.login=Login + +# /card.html +card.moveCard=Move card +card.inColumn=In column: +card.in=in +card.archive=Archive +card.backlog=Backlog +card.trash=Trash +card.move=Move +card.dates=Dates +card.created=Created: {{date}} +card.lastUpdated=Last Updated On: {{date}} +card.lastUpdatedBy=Last Updated By: +card.dueDate=Due Date: {{date}} +card.setDueDate=Set due date +card.people=People +card.watch=Watch +card.unwatch=Unwatch +card.takeCard=Take card +card.surrenderCard=Surrender card +card.assign=Assign +card.createdBy=Created by: +card.assignedTo=Assigned to: +card.watchedBy=Watched By: +card.milestone=Milestone +card.configure=Configure +card.partOf=Part of: +card.description=Description +card.onDate=on +card.updateCount=Updated 1 time +card.updateCounts=Updated {{count}} times +card.lastUpdatedBy=Last updated by +card.closePreview=Close Preview +card.comments=Comments +card.files=Files +card.activity=Activity +card.deleteThisComment=Delete this comment? +card.deleteThisActionItem=Delete this action item? +card.deleteThisActionList=Delete this action list and all its content? +card.addLabel=Add label +card.labels=Labels +card.waitingForUpload=Waiting for upload +card.uploading=Uploading: +card.deleteThisFile=Delete this file? +card.upload=Upload +card.permalink=Permalink +card.description.placeholder=Card description +card.comment.placeholder=Comment +card.name.placeholder=Name +card.actionItem.placeholder=Action Item +card.itemName.placeholder=Item name +card.newList.placeholder=New list name +card.inColumn=in column: + + +# /home.html +partials.home.createNewProject=Create new project +partials.home.newProjectForm.name=Name: +partials.home.newProjectForm.shortName=Short Name: +partials.home.newProjectForm.description=Description: +partials.home.newProjectForm.create=Create new project +partials.home.newProjectForm.cancel=Cancel +partials.home.openTask=My Open Tasks +partials.home.noTask=It's lonely here... +partials.home.title=My Dashboard + +# /board.html +partials.board.none=None +partials.board.archive=Archive +partials.board.backlog=Backlog +partials.board.trash=Trash +partials.board.breadcrumb.board=Board: +partials.board.button.edit=Edit +partials.board.selection.select=Select +partials.board.selection.all=All +partials.board.selection.none=None +partials.board.bulkop.cancel=Cancel +partials.board.bulkop.do=Do +partials.board.bulkop.assignTo=Assign to +partials.board.bulkop.removeAssign=Remove assign +partials.board.bulkop.reassign=Reassign to +partials.board.bulkop.setDueDate=Set due date +partials.board.bulkop.removeDueDate=Remove due date +partials.board.bulkop.setMilestone=Set/Update milestone +partials.board.bulkop.removeMilestone=Remove milestone +partials.board.bulkop.move=Move +partials.board.bulkop.moveArchive=Move to Archive +partials.board.bulkop.moveBacklog=Move to Backlog +partials.board.bulkop.moveTrash=Move to Trash +partials.board.bulkop.addLabel=Add label +partials.board.bulkop.removeLabel=Remove label + +# /me.html +partials.me.provider=Provider: +partials.me.memberSince=Member since: +partials.me.displayName=Display name +partials.me.email=Email +partials.me.enableEmailNotification=Enable email notification: +partials.me.update=Update +partials.me.displayName.placeholder=Your name +partials.me.email.placeholder=your@email.com +partials.me.rememberMe=Remember me: +partials.me.clearAllTokens=Clear All Tokens + +# /search.html +partials.search.title=Global Search + +# /user.html +partials.user.tab.profile=Profile +partials.user.tab.projects=Projects +partials.user.tab.activity=Activity +partials.user.tab.edit-profile=Edit Profile +partials.user.title.profile=profile +partials.user.provider=Provider: +partials.user.memberSince=Member since: +partials.user.username=Username: +partials.user.displayName=Display name: +partials.user.email=Email: +partials.user.mostActiveProjects=Most active projects +partials.user.boards=Boards +partials.user.latestActivity=Latest activity +partials.user.generalInfo=General information + +#--------------------------------------------------------------------------------------------------- + +# /partials/fragments/stats-panel.html +partials.fragments.stats-panel.open=Open: +partials.fragments.stats-panel.closed=Closed: +partials.fragments.stats-panel.backlog=Backlog: +partials.fragments.stats-panel.deferred=Deferred: + +# /partials/fragments/board-sidebar-fragment.html +partials.fragments.board-sidebar-fragment.older=Older +partials.fragments.board-sidebar-fragment.newer=Newer + +# /partials/fragments/bulk-action-modal.html +partials.fragments.bulk-action-modal.title=Bulk operation: +partials.fragments.bulk-action-modal.assignTo=Assign to: +partials.fragments.bulk-action-modal.assign=Assign +partials.fragments.bulk-action-modal.removeAssign=Remove assign from: +partials.fragments.bulk-action-modal.remove=Remove +partials.fragments.bulk-action-modal.reassignTo=Reassign to: +partials.fragments.bulk-action-modal.reassign=Re assign +partials.fragments.bulk-action-modal.setDueDate=Set due date: +partials.fragments.bulk-action-modal.set=Set +partials.fragments.bulk-action-modal.removeDueDate=Remove due date: +partials.fragments.bulk-action-modal.apply=Apply +partials.fragments.bulk-action-modal.setUpdateMilestone=Set/Update milestone: +partials.fragments.bulk-action-modal.removeMilestone=Remove milestone +partials.fragments.bulk-action-modal.addLabel=Add label +partials.fragments.bulk-action-modal.removeLabel=Remove label +partials.fragments.bulk-action-modal.cancel=Cancel + +# /partials/fragments/card-activity-entry-fragment.html +partials.fragments.card-activity-entry-fragment.CARD_MOVE.1=moved the card from +partials.fragments.card-activity-entry-fragment.CARD_MOVE.2=to +partials.fragments.card-activity-entry-fragment.CARD_CREATE=created the card {{name}} in +partials.fragments.card-activity-entry-fragment.CARD_UPDATE=changed the card name to {{name}} +partials.fragments.card-activity-entry-fragment.CARD_ARCHIVE=moved the card to the archive +partials.fragments.card-activity-entry-fragment.CARD_BACKLOG=moved the card to the backlog +partials.fragments.card-activity-entry-fragment.CARD_TRASH=moved the card to the trash +partials.fragments.card-activity-entry-fragment.COMMENT_CREATE=added the comment +partials.fragments.card-activity-entry-fragment.COMMENT_UPDATE=updated the comment +partials.fragments.card-activity-entry-fragment.COMMENT_DELETE=removed a comment +partials.fragments.card-activity-entry-fragment.DESCRIPTION_CREATE=added a description +partials.fragments.card-activity-entry-fragment.DESCRIPTION_UPDATE=updated the description +partials.fragments.card-activity-entry-fragment.FILE_UPLOAD=attached +partials.fragments.card-activity-entry-fragment.FILE_DELETE=removed +partials.fragments.card-activity-entry-fragment.ACTION_LIST_CREATE=created the action list +partials.fragments.card-activity-entry-fragment.ACTION_LIST_DELETE=deleted the action list +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_CREATE.1=added +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_CREATE.2=to +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_DELETE.1=deleted +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_DELETE.2=from +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_MOVE.1=moved +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_MOVE.2=to +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_CHECK.1=completed +partials.fragments.card-activity-entry-fragment.ACTION_ITEM_CHECK.2=in + +# /partials/fragments/card-activity-label-name-value-fragment.html +partials.fragments.card-activity-label-name-value-fragment.LABEL_CREATE.MILESTONE=added to milestone +partials.fragments.card-activity-label-name-value-fragment.LABEL_CREATE.DUE_DATE=added a due date for +partials.fragments.card-activity-label-name-value-fragment.LABEL_CREATE.ASSIGNED=assigned the card to +partials.fragments.card-activity-label-name-value-fragment.LABEL_CREATE.WATCHED_BY=is now watching this card +partials.fragments.card-activity-label-name-value-fragment.LABEL_CREATE.default=added the label {{name}} +partials.fragments.card-activity-label-name-value-fragment.LABEL_DELETE.MILESTONE=removed from milestone +partials.fragments.card-activity-label-name-value-fragment.LABEL_DELETE.DUE_DATE=removed a due date for +partials.fragments.card-activity-label-name-value-fragment.LABEL_DELETE.ASSIGNED.1=removed +partials.fragments.card-activity-label-name-value-fragment.LABEL_DELETE.ASSIGNED.2=from the assigned users +partials.fragments.card-activity-label-name-value-fragment.LABEL_DELETE.WATCHED_BY=is not watching this card anymore +partials.fragments.card-activity-label-name-value-fragment.LABEL_DELETE.default=removed the label {{name}} + +# /partials/fragments/card-fragment.html +partials.fragments.card-fragment.moveToColumn=Move card to column +partials.fragments.card-fragment.moveToArchive=Move to Archive +partials.fragments.card-fragment.moveToBacklog=Move to Backlog +partials.fragments.card-fragment.moveToTrash=Move to Trash +partials.fragments.card-fragment.watch=Watch +partials.fragments.card-fragment.unwatch=Unwatch +partials.fragments.card-fragment.take=Take +partials.fragments.card-fragment.surrender=Surrender + +# /partials/fragments/columns-fragment.html +partials.fragments.columns-fragment.save=Save +partials.fragments.columns-fragment.cancel=Cancel +partials.fragments.columns-fragment.addCard=Add card +partials.fragments.columns-fragment.selectAll=Select all +partials.fragments.columns-fragment.selectNone=Select none +partials.fragments.columns-fragment.columnStatus=Column Status +partials.fragments.columns-fragment.moveAllToArchive=Move all cards to Archive +partials.fragments.columns-fragment.moveAllToBacklog=Move all cards to Backlog +partials.fragments.columns-fragment.moveAllToTrash=Move all cards to Trash +partials.fragments.columns-fragment.moveColumnToArchive=Move column to Archive +partials.fragments.columns-fragment.moveColumnToBacklog=Move column to Backlog +partials.fragments.columns-fragment.moveColumnToTrash=Move column to Trash +partials.fragments.columns-fragment.add=Add +partials.fragments.columns-fragment.cardName.placeholder=Card name +partials.fragments.columns-fragment.columnName.placeholder=Column name + +# /partials/fragments/common-manage-roles-modal.html +partials.fragments.common-manage-roles-modal.applicationPermissions=Application Permissions +partials.fragments.common-manage-roles-modal.projectPermissions=Project Permissions +partials.fragments.common-manage-roles-modal.boardPermissions=Board Permissions +partials.fragments.common-manage-roles-modal.columnPermissions=Column Permissions +partials.fragments.common-manage-roles-modal.cardPermissions=Card Permissions +partials.fragments.common-manage-roles-modal.users=Users +partials.fragments.common-manage-roles-modal.noUsersIn=No users in {{name}} +partials.fragments.common-manage-roles-modal.usersIn=Users in {{name}} +partials.fragments.common-manage-roles-modal.otherUsersMatching=Other users matching {{username}} +partials.fragments.common-manage-roles-modal.searchAUser=Search a user + +partials.fragments.common-manage-roles.confirmation.title=Unsaved Changes +partials.fragments.common-manage-roles.confirmation.content=Saves changes for {{roleName}} + +# /partials/fragments/common-manage-roles.html +partials.fragments.common-manage-roles.searchUsername=Type a Username +partials.fragments.common-manage-roles.username=Username +partials.fragments.common-manage-roles.roleName=Role Name +partials.fragments.common-manage-roles.deleteThisRole=Delete this role? + +# /partials/fragments/confirm-modal-fragment.html + +partials.fragments.confirm-modal-fragment.operation.move-card-from-column-to-location=Move cards from column {{columnName}} to {{location}}? +partials.fragments.confirm-modal-fragment.operation.move-column-to-location=Move column {{columnName}} and his content to {{location}}? +partials.fragments.confirm-modal-fragment.operation.move-card-to-location=Move selected cards to {{location}}? +partials.fragments.confirm-modal-fragment.confirm=Confirm +partials.fragments.confirm-modal-fragment.cancel=Cancel + +# /partials/fragments/create-board-fragment.html +partials.fragments.create-board-fragment.createNewBoard=Create new board +partials.fragments.create-board-fragment.name=Name: +partials.fragments.create-board-fragment.shortName=Short name: +partials.fragments.create-board-fragment.description=Description: +partials.fragments.create-board-fragment.createNewBoard=Create new board +partials.fragments.create-board-fragment.cancel=Cancel + +# /partials/fragments/label-pickers.html +partials.fragments.label-pickers.value=Value +partials.fragments.label-pickers.date=Date +partials.fragments.label-pickers.card=Card +partials.fragments.label-pickers.user=User + +# /partials/fragments/lavagna-search.html +partials.fragments.lavagna-search.search=Search + +# /partials/fragments/move-card-to-column-modal-fragment.html +partials.fragments.move-card-to-column-modal-fragment.moveCard=Move card: +partials.fragments.move-card-to-column-modal-fragment.toColumn=To column: +partials.fragments.move-card-to-column-modal-fragment.move=Move +partials.fragments.move-card-to-column-modal-fragment.cancel=Cancel + +# /partials/fragments/notifications-fragment.html +partials.fragments.notifications-fragment.dismiss=Dismiss +partials.fragments.notifications-fragment.undo=Undo + +# /partials/fragments/search-result-fragment.html +partials.fragments.search-result-fragment.foundCount=Found: {{count}} +partials.fragments.search-result-fragment.selectAllInPage=Select all in page +partials.fragments.search-result-fragment.deselectAllInPage=Deselect all in page +partials.fragments.search-result-fragment.assignTo=Assign to: +partials.fragments.search-result-fragment.assign=Assign +partials.fragments.search-result-fragment.removeAssignFrom=Remove assign from: +partials.fragments.search-result-fragment.remove=Remove +partials.fragments.search-result-fragment.reassignTo=Reassign to: +partials.fragments.search-result-fragment.reassign=Reassign +partials.fragments.search-result-fragment.setDueDate=Set due date: +partials.fragments.search-result-fragment.set=Set +partials.fragments.search-result-fragment.removeDueDate=Remove due date: +partials.fragments.search-result-fragment.remove=Remove +partials.fragments.search-result-fragment.setUpdateMilestone=Set/Update milestone: +partials.fragments.search-result-fragment.removeMilestone=Remove milestone + +# /partials/fragments/lavagna-sidebar.html +partials.fragments.sidebar.home=My Dashboard +partials.fragments.sidebar.profile=My Profile +partials.fragments.sidebar.settings=Admin Panel +partials.fragments.sidebar.tasks=My Tasks +partials.fragments.sidebar.logout=Log Out + +# /partials/fragments/lavagna-navigation.html +partials.project.fragments.navbar.project=Home +partials.project.fragments.navbar.milestones=Milestones +partials.project.fragments.navbar.statistics=Statistics +partials.project.fragments.nabvar.admin=Settings +partials.project.fragments.nabvar.admin.project=Project +partials.project.fragments.nabvar.admin.boards=Boards +partials.project.fragments.nabvar.admin.anonymousUsersAccess=Anonymous Access +partials.project.fragments.navbar.admin.roles=Roles +partials.project.fragments.navbar.admin.labels=Labels +partials.project.fragments.navbar.admin.milestones=Milestones +partials.project.fragments.navbar.admin.columns=Columns +partials.project.fragments.navbar.admin.import=Import + + +partials.admin.fragments.navbar.title=Admin Panel +partials.admin.fragments.navbar.admin=Home +partials.admin.fragments.navbar.admin.adminShowAllParameters=Parameters +partials.admin.fragments.navbar.admin.adminConfigureLogin=Login +partials.admin.fragments.navbar.admin.adminManageSmtpConfiguration=SMTP +partials.admin.fragments.navbar.admin.adminManageAnonymousUsersAccess=Anonymous Access +partials.admin.fragments.navbar.admin.adminManageUsers=Users +partials.admin.fragments.navbar.admin.adminRole=Roles +partials.admin.fragments.navbar.admin.adminExportImport=Export and Import +partials.admin.fragments.navbar.admin.adminEndpointInfo=Endpoint + +partials.lvg-tab-menu.more=More + +#/partials/fragments/bulk-action-inline.html +partials.fragments.bulk-action-inline.assign=Assign +partials.fragments.bulk-action-inline.remove=Remove +partials.fragments.bulk-action-inline.reassign=Reassign +partials.fragments.bulk-action-inline.add=Add +partials.fragments.bulk-action-inline.set=Set +partials.fragments.bulk-action-inline.confirm=Confirm +partials.fragments.bulk-action-inline.select=Select + +#--------------------------------------------------------------------------------------------------- + +# /partials/admin/admin.html +partials.admin.admin.sidebar.home=Home +partials.admin.admin.sidebar.parameters=Parameters +partials.admin.admin.sidebar.configureLogin=Login +partials.admin.admin.sidebar.configureSMTP=SMTP +partials.admin.admin.sidebar.anonymousAccess=Anonymous Access +partials.admin.admin.sidebar.users=Users +partials.admin.admin.sidebar.rolesAndPermissions=Role and permissions +partials.admin.admin.sidebar.exportAndImport=Export and Import +partials.admin.admin.sidebar.endpointInfo=Endpoint +partials.admin.admin.httpsRelatedProblems=Https related configuration problems! + +# /partials/admin/configure-login.html +partials.admin.configure-login.title=Configure login +partials.admin.configure-login.demo=Demo +partials.admin.configure-login.ldap=LDAP +partials.admin.configure-login.ldap.serverUrl=Server URL +partials.admin.configure-login.ldap.managerDn=Manager DN +partials.admin.configure-login.ldap.managerPassword=Manager Password +partials.admin.configure-login.ldap.userSearchBase=User Search Base +partials.admin.configure-login.ldap.userSearchFilter=User Search Filter +partials.admin.configure-login.ldap.checkConfiguration=Verify LDAP Configuration +partials.admin.configure-login.ldap.username=Username +partials.admin.configure-login.ldap.password=Password +partials.admin.configure-login.ldap.check=Verify +partials.admin.configure-login.ldap.allOk=Success! +partials.admin.configure-login.ldap.error=Error, message: +partials.admin.configure-login.persona=Persona +partials.admin.configure-login.persona.audience=Audience +partials.admin.configure-login.persona.help=The protocol, domain name, and port of your site. For example, "https://example.com:443". See the Mozilla Persona documentation. The proposed default value is most probably the correct one. Change if necessary. +partials.admin.configure-login.oauth=OAuth +partials.admin.configure-login.oauth.baseUrl=Base URL for callback +partials.admin.configure-login.oauth.apiKey=API Key +partials.admin.configure-login.oauth.secret=Secret +partials.admin.configure-login.oauth.callback=Callback URL + +# /partials/admin/endpoint-info.html +partials.admin.endpoint-info.title=Endpoint informations +partials.admin.endpoint-info.withoutPermissions={{count}} endpoints without permission +partials.admin.endpoint-info.noMethodSpecified=No method specified +partials.admin.endpoint-info.permission=Permission: +partials.admin.endpoint-info.missingPermission=Missing permission +partials.admin.endpoint-info.handler=Handler: +partials.admin.endpoint-info.missingPathCheck=Missing path check +partials.admin.endpoint-info.path-check=Path checks + +# /partials/admin/export-import.html +partials.admin.export-import.export=Export +partials.admin.export-import.exportButton=Export +partials.admin.export-import.import=Import +partials.admin.export-import.overrideConf=Override current configuration with the imported one +partials.admin.export-import.importButton=Import + +# /partials/admin/manage-anonymous-users-access.html +partials.admin.manage-anonymous-users-access.title=Anonymous Users Access +partials.admin.manage-anonymous-users-access.globalAccess=Global Access + +# /partials/admin/manage-smtp-configuration.html +partials.admin.manage-smtp-configuration.title=E-Mail notifications +partials.admin.manage-smtp-configuration.host=Host +partials.admin.manage-smtp-configuration.port=Port +partials.admin.manage-smtp-configuration.protocol=Protocol +partials.admin.manage-smtp-configuration.username=Username +partials.admin.manage-smtp-configuration.password=Password +partials.admin.manage-smtp-configuration.from=From E-Mail +partials.admin.manage-smtp-configuration.properties=Properties +partials.admin.manage-smpt-configuration.emailAddressTest=E-Mail address +partials.admin.manage-smpt-configuration.sendTestEmail=Send test E-Mail +partials.admin.manage-smpt-configuration.send=Send +partials.admin.manage-smpt-configuration.update=Update + +# /partials/admin/manage-users.html +partials.admin.manage-users.title=Manage users +partials.admin.manage-users.addUser=Add user +partials.admin.manage-users.provider=Account provider +partials.admin.manage-users.username=Username +partials.admin.manage-users.email=Email +partials.admin.manage-users.displayName=Display name +partials.admin.manage-users.roles=Roles +partials.admin.manage-users.enabled=Enabled +partials.admin.manage-users.cancel=Cancel +partials.admin.manage-users.bulkImport=Bulk import users +partials.admin.manage-users.import=Import +partials.admin.manage-users.users=Users +partials.admin.manage-users.accountProvider=Account provider + +# /partials/admin/parameters.html +partials.admin.parameters.title=Configuration parameters +partials.admin.parameters.key=Key +partials.admin.parameters.value=Value + + +#--------------------------------------------------------------------------------------------------- + + +# /partials/project/manage-anonymous-users-access.html +partials.project.manage-anonymous-users-access.title=Anonymous User Access +partials.project.manage-anonymous-users-access.description=If the anonymous user is not enabled globally, here you can enable it at a project level basis. Check in the admin if the anonymous user support has been enabled. + +# /partials/project/manage-columns-status.html + +# /partials/project/manage-home.html +partials.project.manage-home.name=Name: +partials.project.manage-home.description=Description: +partials.project.manage-home.update=Update +partials.project.manage-home.archived=Archived: +partials.project.manage-home.boards=Boards: +partials.project.manage-home.edit=Edit +partials.project.manage-home.cancel=Cancel + +# /partials/project/manage-import.html +partials.project.manage-import.trello=Trello +partials.project.manage-import.notes=You can import your board from Trello. Please note that currently it will import only some part of your boards (columns, cards, comments, checklists, members, due date) and the creator will be the user used for the import. Currently the archive is skipped. Once the board has been imported, it cannot be re-imported. A better solution is in the roadmap. +partials.project.manage-import.failedImport=Failed to load the trello script +partials.project.manage-import.import=Import +partials.project.manage-import.connect=Connect to Trello +partials.project.manage-import.noKeySet=No TRELLO_API_KEY defined. +partials.project.manage-import.progressMessage=Importing + +# /partials/project/manage-labels.html +partials.project.manage-labels.editList=List Values +partials.project.manage-labels.valuesList=Values: +partials.project.manage-labels.name=Name: +partials.project.manage-labels.type=Type: +partials.project.manage-labels.unique=Unique per Card +partials.project.manage-labels.color=Color: +partials.project.manage-labels.addLabel=Add Label +partials.project.manage-labels.createLabel=Create New Label +partials.project.manage-labels.deleteLabel=Do you want to delete this label? +partials.project.manage-labels.labelName.placeholder=Label name +partials.project.manage-labels.newListValue.placeholder=New value +partials.project.manage-labels.types.NULL=No Value +partials.project.manage-labels.types.STRING=Text +partials.project.manage-labels.types.TIMESTAMP=Date +partials.project.manage-labels.types.INT=Number +partials.project.manage-labels.types.CARD=Card +partials.project.manage-labels.types.USER=User +partials.project.manage-labels.types.LIST=List + +# /partials/project/manage-milestones.html +partials.project.manage-milestones.title=Settings: Milestones +partials.project.manage-milestones.add=Add +partials.project.manage-milestones.newMilestone=New Milestone +partials.project.manage-milestones.remove=Delete this milestone? + +# /partials/project/manage-roles.html +partials.project.manage-roles.title=Administration: Roles and Permissions + + +# /partials/project/milestones.html +partials.project.milestones.breadcrumb.page=Milestones +partials.project.milestones.noCards=No cards associated with this milestone. +partials.project.milestones.assignedAndClosedCards=Assigned vs closed +partials.project.milestones.cards=Cards + +# /partials/project/statistics.html +partials.project.statistics.breadcrumb.page=Statistics +partials.project.statistics.cardsHistory=Cards history +partials.project.statistics.cardsByLabel=Cards by Label +partials.project.statistics.activeUsers=Active users +partials.project.statistics.averageUsersPerCard=Average users per card +partials.project.statistics.averageCardsPerUser=Average cards per user +partials.project.statistics.noData=Not enough data to display charts +partials.project.statistics.open=Open +partials.project.statistics.closed=Closed +partials.project.statistics.backlog=Backlog +partials.project.statistics.deferred=Deferred +partials.project.statistics.totalCards=Total cards +partials.project.statistics.createdThisPeriod=Created this period +partials.project.statistics.closedThisPeriod=Closed this period +partials.project.statistics.filterByBoard=Filter by board: +partials.project.statistics.dateRange=Date range: +partials.project.statistics.last30Days=Last 30 days +partials.project.statistics.last14Days=Last 14 days +partials.project.statistics.last7Days=Last 7 days +partials.project.statistics.thisMonth=This month +partials.project.statistics.thisWeek=This week +partials.project.statistics.mostActiveCard=Most active card +partials.project.statistics.createdAndClosedCards=Created vs closed + +# /partials/project/project.html +partials.project.project.breadcrumb.home=Home +partials.project.project.breadcrumb.project=Project: {{name}} +partials.project.project.myOpenTasks=My Open Tasks in {{name}} +partials.project.project.noTask=It's lonely here... + +# /partials/project/search.html +partials.project.search.title=Project Search + +#--------------------------------------------------------------------------------------------------- +# Events +event.CARD_UPDATE=User {0} has updated card name to: {1} +event.COMMENT_CREATE=User {0} has added the comment:\n{1} +event.DESCRIPTION_CREATE=User {0} has added the description:\n{1} +event.COMMENT_UPDATE=User {0} has updated the comment to:\n{1}\nfrom:\n{2} +event.COMMENT_DELETE=User {0} has deleted the comment:\n{1} +event.DESCRIPTION_UPDATE=User {0} has updated the description to:\n{1}\nfrom:\n{2} +event.CARD_ARCHIVE=User {0} has moved the card from column: {1} to the archive (column: {2}) +event.CARD_BACKLOG=User {0} has moved the card from column: {1} to the backlog (column: {2}) +event.CARD_TRASH=User {0} has moved the card from column: {1} to the trash (column: {2}) +event.CARD_MOVE=User {0} has moved the card from column: {1} to column: {2} +event.FILE_UPLOAD=User {0} has uploaded the file {1} +event.FILE_DELETE=User {0} has deleted the file {1} +event.ACTION_LIST_CREATE=User {0} has created an action list named: {1} +event.ACTION_LIST_DELETE=User {0} has deleted the action list: {1} +event.ACTION_ITEM_CREATE=User {0} has created an item named: {1} in the action list {2} +event.ACTION_ITEM_DELETE=User {0} has deleted the item: {1} in the action list {2} +event.ACTION_ITEM_CHECK=User {0} has checked the item: {1} in the action list {2} +event.ACTION_ITEM_UNCHECK=User {0} has unchecked the item: {1} in the action list {2} +event.ACTION_ITEM_MOVE=User {0} has moved the item: {1} in the action list {2} +event.LABEL_CREATE.MILESTONE=User {0} has added a Milestone: {1} +event.LABEL_CREATE.DUE_DATE=User {0} added a due date for: {1} +event.LABEL_CREATE.ASSIGNED=User {0} assigned the card to: {1} +event.LABEL_CREATE.WATCHED_BY=User {0} is now watching this card +event.LABEL_CREATE=User {0} added label: {1} +event.LABEL_DELETE.MILESTONE=User {0} removed a Milestone: {1} +event.LABEL_DELETE.DUE_DATE=User {0} removed a due date for {1} +event.LABEL_DELETE.ASSIGNED=User {0} removed {1} from the assigned users +event.LABEL_DELETE.WATCHED_BY=User {0} is not watching this card anymore +event.LABEL_DELETE=User {0} removed label: {1} + + + +#--------------------------------------------------------------------------------------------------- +#Notifications +notification.error.connectionFailure=Connection failure. Please reload the page. +notification.error.file-size-too-big=File size is too big. Max allowed is {{maxSize}} bytes + +notification.card.COMMENT_DELETE.success=Comment deleted successfully +notification.card.COMMENT_DELETE.error=Failed to delete the comment +notification.card.ACTION_LIST_DELETE.success=Action list deleted successfully +notification.card.ACTION_LIST_DELETE.error=Failed to delete the action list +notification.card.ACTION_ITEM_DELETE.success=Action item deleted successfully +notification.card.ACTION_ITEM_DELETE.error=Failed to delete the action item +notification.card.LABEL_DELETE.success=Label [{{labelName}}] has been deleted +notification.card.LABEL_DELETE.error=Error deleting Label [{{labelName}}] +notification.card.FILE_DELETE.success=File has been deleted +notification.card.FILE_DELETE.error=Failed to delete file +notification.card.moveToColumn.success=Card has been successfully moved to column {{columnName}} +notification.card.moveToColumn.error=Failed to move the card +notification.card.moveToLocation.success=Card has been successfully moved to {{location}} +notification.card.moveToLocation.error=Failed to move the card + +notification.project.creation.success=Project created successfully +notification.project.creation.error=Failed to create the new project + +notification.board.creation.success=Board created successfully +notification.board.creation.error=Failed to create the new board + +notification.user.update.success=User profile updated successfully +notification.user.update.error=Failed to update the user profile +notification.user.tokenCleared.success=All your login tokens have been removed +notification.user.tokenCleared.error=Failed to clear your login tokens + +notification.admin-manage-users.add.error=Failed to add a new user +notification.admin-manage-users.bulkImport.success=Users imported successfully +notification.admin-manage-users.bulkImport.error=Failed to import users +notification.admin-manage-users.toggle.error=Failed to update the user's status +notification.admin-export-import.import.success=Backup imported successfully +notification.admin-export-import.import.error=Failed to import the backup + +notification.manage-role.createRole.error=Failed to create a new role +notification.manage-role.deleteRole.error=Failed to delete the role {{roleName}} +notification.manage-role.addUserToRole.error=Failed to assign user [{{username}}] to role [{{roleName}}] +notification.manage-role.removeUserToRole.error=Failed to remove user [{{username}}] from role [{{roleName}}] +notification.manage-role.updateRole.error=Failed to update role {{roleName}} + +notification.admin-configure-login.updateActiveProviders.error=Failed to update the login configuration +notification.admin-configure-login.saveLdapConfig.success=LDAP login configuration update successfully +notification.admin-configure-login.saveLdapConfig.error=Failed to update the LDAP login configuration +notification.admin-configure-login.savePersonaConfig.success=Persona login configuration update successfully +notification.admin-configure-login.savePersonaConfig.error=Failed to update the Persona login configuration +notification.admin-configure-login.saveOAuthConfig.success=OAuth login configuration update successfully +notification.admin-configure-login.saveOAuthConfig.error=Failed to update the OAuth login configuration +notification.admin-configure-login.updateAnonConfiguration.error=Failed to update the anonymous user access configuration + +notification.admin-manage-smtp-configuration.updateConfiguration.error=Failed to update the SMTP configuration +notification.admin-manage-smtp-configuration.saveSmtpConfig.success=SMTP configuration updated successfully +notification.admin-manage-smtp-configuration.saveSmtpConfig.error=Failed to update the SMTP configuration + +notification.admin-parameters.updateConfiguration.success=Configuration parameter {{parameterName}} updated successfully +notification.admin-parameters.updateConfiguration.error=Failed to update configuration parameter {{parameterName}} +notification.admin-parameters.deleteConfiguration.success=Configuration parameter {{parameterName}} deleted successfully +notification.admin-parameters.deleteConfiguration.error=Failed to delete configuration parameter {{parameterName}} + +notification.project-manage-home.update.success=Project updated successfully +notification.project-manage-home.update.error=Failed to update the project configuration + +notification.project-manage-boards.update.success=Board updated successfully +notification.project-manage-boards.update.error=Failed to update the board configuration +notification.project-manage-boards.archive.success=Board archived successfully +notification.project-manage-boards.archive.error=Failed to archive the board +notification.project-manage-boards.unarchive.success=Board restored successfully +notification.project-manage-boards.unarchive.error=Failed to restore the board + +notification.project-manage-columns-status.update.success=Column definition updated successfully +notification.project-manage-columns-status.update.error=Failed to update the column definition + +notification.project-manage-labels.remove.success=Label deleted successfully +notification.project-manage-labels.remove.error=Failed to delete the label +notification.project-manage-labels.update.success=Label updated successfully +notification.project-manage-labels.update.error=Failed to update the label +notification.project-manage-labels.add.success=Label added successfully +notification.project-manage-labels.add.error=Failed to add a new label + +notification.project-manage-milestones.remove.success=Milestone deleted successfully +notification.project-manage-milestones.remove.error=Failed to delete the milestone +notification.project-manage-milestones.update.success=Milestone updated successfully +notification.project-manage-milestones.update.error=Failed to update the milestone + +notification.project-manage-import.importFromTrello.success=Successfully imported the data from Trello +notification.project-manage-import.importFromTrello.error=Failed to import the data from Trello + +notification.project-manage-anon.enable.error=Failed to enable anonymous user access +notification.project-manage-anon.disable.error=Failed to disable anonymous user access + + diff --git a/src/main/resources/io/lavagna/notification/email.html b/src/main/resources/io/lavagna/notification/email.html new file mode 100644 index 000000000..ffb83df32 --- /dev/null +++ b/src/main/resources/io/lavagna/notification/email.html @@ -0,0 +1,27 @@ + + + + + +
+ {{#cards}} +
+

+ {{cardName}} +

+
+ {{#cardEvents}} +
+
{{.}}
+
+ {{/cardEvents}} +
+
+ {{/cards}} +
+ + \ No newline at end of file diff --git a/src/main/resources/io/lavagna/notification/email.txt b/src/main/resources/io/lavagna/notification/email.txt new file mode 100644 index 000000000..38c86bc1e --- /dev/null +++ b/src/main/resources/io/lavagna/notification/email.txt @@ -0,0 +1,10 @@ +{{#cards}} +## {{cardName}} ({{baseApplicationUrl}}{{cardFull.projectShortName}}/{{cardFull.boardShortName}}-{{cardFull.sequence}}) + +{{#cardEvents}} +{{.}} + +{{/cardEvents}} + + +{{/cards}} \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 000000000..175e62352 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/views/login.html b/src/main/webapp/WEB-INF/views/login.html new file mode 100644 index 000000000..34f788595 --- /dev/null +++ b/src/main/webapp/WEB-INF/views/login.html @@ -0,0 +1,218 @@ + + + + + + + +Login in Lavagna + + + + +
+
+ + +
+
+ Lavagna +
+ +
+ + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/views/logout-persona.html b/src/main/webapp/WEB-INF/views/logout-persona.html new file mode 100644 index 000000000..255653f1b --- /dev/null +++ b/src/main/webapp/WEB-INF/views/logout-persona.html @@ -0,0 +1,26 @@ + + + + + + + + + + +logout... + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..b948ad9fa --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,11 @@ + + + + + 15 + + + \ No newline at end of file diff --git a/src/main/webapp/app/app.js b/src/main/webapp/app/app.js new file mode 100644 index 000000000..4c9c9615b --- /dev/null +++ b/src/main/webapp/app/app.js @@ -0,0 +1,293 @@ +(function() { + + 'use strict'; + + //declare all the modules here + angular.module('lavagna.controllers', [ 'lavagna.services' ]); + angular.module('lavagna.directives', [ 'lavagna.services' ]); + angular.module('lavagna.filters', []); + angular.module('lavagna.services', []); + // + + var module = angular.module('lavagna', [ 'ui.router', 'lavagna.services', + 'lavagna.controllers', 'lavagna.filters', 'lavagna.directives', + 'ngSanitize', 'ui.sortable', 'pasvaz.bindonce', 'ui.bootstrap', + 'pascalprecht.translate', 'digitalfondue.dftabmenu', 'digitalfondue.dfautocomplete', + 'angularFileUpload', 'angularUUID2' ]); + + module.constant('CONTEXT_PATH', document.getElementsByTagName("base")[0].href); + + // http://www.rosher.co.uk/post/2014/03/26/Angular-Tips-Measuring-Rendering-Performance.aspx + /*module.run(['$rootScope', function($rootScope) { + var $oldDigest = $rootScope.$digest; + var $newDigest = function() { + console.time("$digest"); + $oldDigest.apply($rootScope); + console.timeEnd("$digest"); + }; + $rootScope.$digest = $newDigest; + }])*/ + + /** + * Configure angular-ui-router here... + */ + module.config(function ($stateProvider, $urlRouterProvider, $locationProvider, $translateProvider) { + + $locationProvider.html5Mode(true); + + + angular.forEach(io_lavagna.i18n, function(map, lang) { + $translateProvider.translations(lang, map) + }); + $translateProvider.preferredLanguage('en'); + $translateProvider.usePostCompiling(true); + + + var cardCtrlResolver = { + card : function(CardCache, $stateParams) { + return CardCache.cardByBoardShortNameAndSeqNr($stateParams.shortName, $stateParams.seqNr); + }, + currentUser : function(User) { + return User.currentCachedUser(); + }, + project : function(ProjectCache, $stateParams) { + return ProjectCache.project($stateParams.projectName); + }, + board : function(BoardCache, $stateParams) { + return BoardCache.board($stateParams.shortName); + } + }; + + var projectResolver = { + project : function(ProjectCache, $stateParams) { + return ProjectCache.project($stateParams.projectName); + } + }; + + $stateProvider.state('home', { + url : '/', + templateUrl : 'partials/home.html', + controller : 'HomeCtrl' + }) + //---- ACCOUNT ---- + .state('account', { + url :'/me/', + templateUrl : 'partials/me.html', + controller: 'AccountCtrl' + }).state('user', { + url :'/user/:provider/:username', + templateUrl : 'partials/user.html', + controller: 'UserCtrl' + }).state('user-projects', { + url :'/user/:provider/:username/projects/', + templateUrl : 'partials/user/projects.html', + controller: 'UserProjectsCtrl' + }).state('user-activity', { + url :'/user/:provider/:username/activity/', + templateUrl : 'partials/user/activity.html', + controller: 'UserActivityCtrl' + }) + //---- SEARCH ---- + .state('globalSearch', { + url : '/search/?q&page', + templateUrl : 'partials/search.html', + controller: 'SearchCtrl', + reloadOnSearch: false + }).state('globalSearch.card', { + url : ':projectName/{shortName:[A-Z0-9_]+}-{seqNr:[0-9]+}', + templateUrl : 'partials/card.html', + controller : 'CardCtrl', + resolve : cardCtrlResolver + }) + //---- ADMIN ---- + .state('admin', { + url: '/admin/', + abstract: true, + templateUrl : 'partials/admin/admin.html', + controller: 'AdminCtrl' + }).state('admin.adminShowAllParameters', { + url: '', + templateUrl: 'partials/admin/parameters.html', + controller: 'AdminParametersCtrl' + }).state('admin.adminRole', { + url: 'role/', + templateUrl : 'partials/fragments/common-manage-roles.html', + controller: 'ManageRoleCtrl' + }).state('admin.adminExportImport', { + url: 'export-import/', + templateUrl : 'partials/admin/export-import.html', + controller : 'AdminExportImportCtrl' + }).state('admin.adminEndpointInfo', { + url: 'endpoint-info/', + templateUrl : 'partials/admin/endpoint-info.html', + controller: 'AdminEndpointInfoCtrl' + }).state('admin.adminManageUsers', { + url: 'manage-users/', + templateUrl : 'partials/admin/manage-users.html', + controller: 'AdminManageUsersCtrl' + }).state('admin.adminConfigureLogin', { + url: 'configure-login/', + templateUrl: 'partials/admin/configure-login.html', + controller: 'AdminConfigureLoginCtrl' + }).state('admin.adminManageSmtpConfiguration', { + url : 'manage-smtp-configuration/', + templateUrl : 'partials/admin/manage-smtp-configuration.html', + controller : 'AdminManageSmtpConfigurationCtrl' + }) + + //---- MANAGE PROJECT ---- + .state('ProjectManage', { + url : '/:projectName/manage/', + templateUrl : 'partials/project/manage.html', + abstract: true, + controller : 'ProjectManageCtrl', + resolve: projectResolver + }).state('ProjectManage.projectRole', { + url : 'roles/', + templateUrl : 'partials/project/manage-roles.html', + controller: 'ManageRoleCtrl' + }).state('ProjectManage.projectImport', { + url : 'import/', + templateUrl : 'partials/project/manage-import.html', + controller: 'ManageImportCtrl', + resolve: projectResolver + }).state('ProjectManage.projectManageLabels', { + url : 'labels/', + templateUrl : 'partials/project/manage-labels.html', + controller : 'ProjectManageLabelsCtrl', + resolve: projectResolver + }).state('ProjectManage.projectManageMilestones', { + url : 'milestones/', + templateUrl : 'partials/project/manage-milestones.html', + controller : 'ProjectManageMilestonesCtrl', + resolve: projectResolver + }).state('ProjectManage.projectManageAnonymousUsers', { + url : 'anonymous-users-access/', + templateUrl : 'partials/project/manage-anonymous-users-access.html', + controller : 'ProjectManageAnonymousUsersAccessCtrl', + resolve: projectResolver + }).state('ProjectManage.projectManageColumnsStatus', { + url : 'columns-status/', + templateUrl : 'partials/project/manage-columns-status.html', + controller : 'ProjectManageColumnsStatusCtrl', + resolve: projectResolver + }).state('ProjectManage.projectManageBoards', { + url : 'boards/', + templateUrl : 'partials/project/manage-boards.html', + controller : 'ProjectManageBoardsCtrl', + resolve: projectResolver + }).state('ProjectManage.projectManageHome', { + url : '', + templateUrl : 'partials/project/manage-home.html', + controller : 'ProjectManageHomeCtrl', + resolve: projectResolver + }) + + // ---- MILESTONES ---- + .state('projectMilestones', { + url : '/:projectName/milestones/', + templateUrl : 'partials/project/milestones.html', + controller : 'ProjectMilestonesCtrl', + resolve : projectResolver + }).state('projectMilestones.card', { + url : '{shortName:[A-Z0-9_]+}-{seqNr:[0-9]+}', + templateUrl : 'partials/card.html', + controller : 'CardCtrl', + resolve : cardCtrlResolver + }) + + + //---- PROJECT ---- + .state('project', { + url : '/:projectName/', + templateUrl : 'partials/project/project.html', + controller : 'ProjectCtrl', + resolve : projectResolver + }).state('projectStatistics', { + url : '/:projectName/statistics/', + templateUrl : 'partials/project/statistics.html', + controller: 'ProjectStatisticsCtrl', + resolve : projectResolver + }).state('projectSearch', { + url : '/:projectName/search/?q&page', + templateUrl : 'partials/project/search.html', + controller: 'SearchCtrl', + reloadOnSearch: false + }).state('projectSearch.card', { + url : '{shortName:[A-Z0-9_]+}-{seqNr:[0-9]+}', + templateUrl : 'partials/card.html', + controller : 'CardCtrl', + resolve : cardCtrlResolver + }).state('projectBoard', { + url : '/:projectName/{shortName:[A-Z0-9_]+}', + templateUrl : 'partials/board.html', + controller : 'BoardCtrl', + resolve : { + project : function(ProjectCache, $stateParams) { + return ProjectCache.project($stateParams.projectName); + }, + board : function(BoardCache, $stateParams) { + return BoardCache.board($stateParams.shortName); + } + } + }).state('projectBoard.card', { + url : '-{seqNr:[0-9]+}', + templateUrl : 'partials/card.html', + controller : 'CardCtrl', + resolve : cardCtrlResolver + }) + // ----------- + .state('404', { + url : '/404/:resourceId', + templateUrl : 'partials/404.html', + controller : 'ErrorCtrl' + }); + + $urlRouterProvider.otherwise('/'); + }); + + //reset the title to "Lavagna" (default). The controller will override with his own value if necessary. + module.run(function($rootScope) { + $rootScope.$on("$stateChangeSuccess", function(event, toState, toParams, fromState, fromParams){ + $rootScope.pageTitle = 'Lavagna' + }); + }) + + /** + * install CSRF token filter. + * + * security filter set the CSRF token in the http header. + * + */ + + module.factory('lavagnaHttpInterceptor', function($q, $window) { + var csrfToken = null; + return { + 'request' : function(config) { + if (csrfToken != null) { + config.headers['x-csrf-token'] = csrfToken; + } + return config; + }, + 'response' : function(response) { + var headers = response.headers(); + if(headers['content-type'] && headers['content-type'].indexOf('application/json') === 0 && headers['x-csrf-token']) { + csrfToken = response.headers()['x-csrf-token']; + $window.csrfToken = csrfToken; + } + return response; + }, + 'responseError': function(rejection) { + //if the session has been lost, trigger a reload + if(rejection.status === 401) { + $window.location.reload(); + } + return $q.reject(rejection); + } + }; + }); + + module.config(function($httpProvider) { + $httpProvider.interceptors.push('lavagnaHttpInterceptor'); + }); +})(); diff --git a/src/main/webapp/app/controllers/account.js b/src/main/webapp/app/controllers/account.js new file mode 100644 index 000000000..661afa225 --- /dev/null +++ b/src/main/webapp/app/controllers/account.js @@ -0,0 +1,49 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AccountCtrl', function ($scope, User, Notification) { + + + User.currentCachedUser().then(function(user) { + $scope.userNameProfile = user.username; + $scope.userProvider = user.provider; + $scope.userUsername = user.username; + }); + + + $scope.profile = {}; + + $scope.isCurrentUser = true; + + var loadUser = function (u) { + $scope.user = u; + $scope.profile.email = u.email; + $scope.profile.displayName = u.displayName; + $scope.profile.emailNotification = u.emailNotification; + + }; + + $scope.clearAllTokens = function () { + User.clearAllTokens().then(function() { + Notification.addAutoAckNotification('success', {key: 'notification.user.tokenCleared.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', {key: 'notification.user.tokenCleared.error'}, false); + }); + }; + + User.current().then(loadUser); + + $scope.update = function (profile) { + User.updateProfile(profile) + .then(User.invalidateCachedUser) + .then(User.current).then(loadUser).then(function() { + Notification.addAutoAckNotification('success', {key: 'notification.user.update.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', {key: 'notification.user.update.error'}, false); + }); + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin-configure-login.js b/src/main/webapp/app/controllers/admin/admin-configure-login.js new file mode 100644 index 000000000..36cd59fdc --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-configure-login.js @@ -0,0 +1,207 @@ +(function() { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + function getPort(window) { + return window.location.port || (window.location.protocol === "https:" ? "443" : "80") + } + + module.controller('AdminConfigureLoginCtrl', function($scope, $window, $modal, Admin, Permission, Notification, CONTEXT_PATH) { + $scope.oauthProviders = ['bitbucket', 'github', 'google', 'twitter']; + + function loadConfiguration() { + Admin.findAllConfiguration().then(function(conf) { + $scope.currentConf = conf; + + var authMethods = JSON.parse(conf['AUTHENTICATION_METHOD']); + $scope.authMethod = {"DEMO": false, "LDAP": false, "PERSONA" : false, "OAUTH" : false}; + for(var i = 0; i < authMethods.length;i++) { + $scope.authMethod[authMethods[i]] = true; + } + + $scope.persona = {audience : conf['PERSONA_AUDIENCE'] || ($window.location.protocol + '//' + $window.location.hostname + ':' + getPort($window))}; + + $scope.ldap = { + serverUrl : conf['LDAP_SERVER_URL'], + managerDn : conf['LDAP_MANAGER_DN'], + managerPassword : conf['LDAP_MANAGER_PASSWORD'], + userSearchBase : conf['LDAP_USER_SEARCH_BASE'], + userSearchFilter: conf['LDAP_USER_SEARCH_FILTER'] + } + + var oauth = conf['OAUTH_CONFIGURATION'] && JSON.parse(conf['OAUTH_CONFIGURATION']) || { providers : []}; + + $scope.oauth = {}; + + angular.forEach($scope.oauthProviders, function(p) { + $scope.oauth[p] = {}; + }); + + $scope.oauth.baseUrl = oauth.baseUrl || CONTEXT_PATH; + + + for(var i = 0; i< oauth.providers.length;i++) { + $scope.oauth[oauth.providers[i].provider] = oauth.providers[i]; + $scope.oauth[oauth.providers[i].provider].present = true; + } + + }); + } + + loadConfiguration(); + + // -- enable providers + + var updateActiveProviders = function(n, o) { + if(n == undefined || o == undefined || n == o) { + return; + } + + var toUpdate = []; + + var activeAuthMethod = []; + for(var k in $scope.authMethod) { + if($scope.authMethod[k]) { + activeAuthMethod.push(k); + } + } + + toUpdate.push({first : 'AUTHENTICATION_METHOD', second : JSON.stringify(activeAuthMethod)}); + + Admin.updateConfiguration({toUpdateOrCreate: toUpdate}).catch(function(error) { + Notification.addAutoAckNotification('error', { key : 'notification.admin-configure-login.updateActiveProviders.error'}, false); + }).then(loadConfiguration); + } + + $scope.$watch('authMethod.DEMO', updateActiveProviders); + + $scope.$watch('authMethod.LDAP', updateActiveProviders); + + $scope.$watch('authMethod.PERSONA', updateActiveProviders); + + $scope.$watch('authMethod.OAUTH', updateActiveProviders); + + // -- save providers config + + $scope.saveLdapConfig = function() { + var toUpdate = []; + + toUpdate.push({first : 'LDAP_SERVER_URL', second : $scope.ldap.serverUrl}); + toUpdate.push({first : 'LDAP_MANAGER_DN', second : $scope.ldap.managerDn}); + toUpdate.push({first : 'LDAP_MANAGER_PASSWORD', second : $scope.ldap.managerPassword}); + toUpdate.push({first : 'LDAP_USER_SEARCH_BASE', second : $scope.ldap.userSearchBase}); + toUpdate.push({first : 'LDAP_USER_SEARCH_FILTER', second : $scope.ldap.userSearchFilter}); + + Admin.updateConfiguration({toUpdateOrCreate: toUpdate}).then(function() { + Notification.addAutoAckNotification('success', { key : 'notification.admin-configure-login.saveLdapConfig.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', { key : 'notification.admin-configure-login.saveLdapConfig.error'}, false); + }).then(loadConfiguration); + }; + + $scope.savePersonaConfig = function() { + var toUpdate = []; + + toUpdate.push({first : 'PERSONA_AUDIENCE', second : $scope.persona.audience}); + + Admin.updateConfiguration({toUpdateOrCreate: toUpdate}).then(function() { + Notification.addAutoAckNotification('success', { key : 'notification.admin-configure-login.savePersonaConfig.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', { key : 'notification.admin-configure-login.savePersonaConfig.error'}, false); + }).then(loadConfiguration); + }; + + function addProviderIfPresent(list, conf, provider) { + if(conf && conf.present) { + list.push({provider: provider, apiKey: conf.apiKey, apiSecret : conf.apiSecret}); + } + } + + $scope.saveOauthConfig = function() { + var toUpdate = []; + + var newOauthConf = {baseUrl: $scope.oauth.baseUrl, providers : []}; + angular.forEach($scope.oauthProviders, function(provider) { + addProviderIfPresent(newOauthConf.providers, $scope.oauth[provider], provider); + }); + toUpdate.push({first : 'OAUTH_CONFIGURATION', second : JSON.stringify(newOauthConf)}); + + Admin.updateConfiguration({toUpdateOrCreate: toUpdate}).then(function() { + Notification.addAutoAckNotification('success', { key : 'notification.admin-configure-login.saveOAuthConfig.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', { key : 'notification.admin-configure-login.saveOAuthConfig.error'}, false); + }).then(loadConfiguration); + }; + + $scope.openLdapConfigModal = function() { + var modalInstance = $modal.open({ + templateUrl: 'partials/admin/fragments/ldap-check-modal.html', + controller: function($scope, $modalInstance, ldapConfig) { + $scope.checkLdapConfiguration = function(ldapConf, ldapCheck) { + Admin.checkLdap(ldapConf, ldapCheck).then(function(r) { + $scope.ldapCheckResult = r; + }); + }; + + $scope.close = function() { + $modalInstance.close('done'); + } + }, + size: 'sm', + windowClass: 'lavagna-modal', + resolve: { + ldapConfig: function () { + return $scope.ldap; + } + } + }); + } + + // ------ Anonymous access + + var load = function () { + Admin.findByKey('ENABLE_ANON_USER').then(function (res) { + $scope.anonEnabled = res.second === "true"; + }); + + Permission.findUsersWithRole('ANONYMOUS').then(function (res) { + var userHasGlobalRole = false; + for (var i = 0; i < res.length; i++) { + if (res[i].provider === 'system' && res[i].username === 'anonymous') { + userHasGlobalRole = true; + } + } + $scope.userHasGlobalRole = userHasGlobalRole; + }); + } + + load(); + + $scope.$watch('anonEnabled', function(newVal, oldVal) { + if(newVal == undefined || oldVal == undefined || newVal == oldVal) { + return; + } + Admin.updateConfiguration( + {toUpdateOrCreate: + [{first: 'ENABLE_ANON_USER', second: newVal.toString()}]} + ).catch(function(error) { + Notification.addAutoAckNotification('error', { key : 'notification.admin-configure-login.updateAnonConfiguration.error'}, false); + }).then(load); + }); + + $scope.$watch('userHasGlobalRole', function(newVal, oldVal) { + if(newVal == undefined || oldVal == undefined || newVal == oldVal) { + return; + } + User.byProviderAndUsername('system', 'anonymous').then(function (res) { + if (newVal) { + Permission.addUserToRole(res.id, 'ANONYMOUS').then(load); + } else { + Permission.removeUserToRole(res.id, 'ANONYMOUS').then(load); + } + }) + }); + }); +})(); diff --git a/src/main/webapp/app/controllers/admin/admin-endpoint-info.js b/src/main/webapp/app/controllers/admin/admin-endpoint-info.js new file mode 100644 index 000000000..7f5b7b9f1 --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-endpoint-info.js @@ -0,0 +1,12 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminEndpointInfoCtrl', function ($scope, Admin) { + Admin.endpointInfo().then(function (res) { + $scope.endpointInfo = res; + }); + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin-export-import.js b/src/main/webapp/app/controllers/admin/admin-export-import.js new file mode 100644 index 000000000..6d55161d4 --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-export-import.js @@ -0,0 +1,69 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminExportImportCtrl', function ($scope, $window, Notification, Admin) { + + $scope.overrideConfiguration = false; + + + $scope.doExport = function (exportConf) { + $("#export-iframe").remove(); + $($window.document.body).append(''); + $window.document.getElementById('export-iframe').contentWindow.document.write('
' + + '
' + + ''); + } + + function importLavagnaCleanUp() { + $scope.importFile = null; + + $scope.$apply(function () { + $scope.importing = false; + }); + } + + function importLavagna(data, status) { + if(status == 200) { + notifySuccess(); + } else { + notifyError(); + } + importLavagnaCleanUp(); + } + + function importLavagnaError() { + notifyError(); + importLavagnaCleanUp(); + } + + function notifySuccess() { + Notification.addAutoAckNotification('success', { + key: 'notification.admin-export-import.import.success' + }, false); + } + + function notifyError() { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-export-import.import.error' + }, false); + } + + $scope.importFile = null; + $scope.onFileSelect = function($files) { + $scope.importFile = $files[0]; //single file + } + + $scope.doImport = function () { + + if ($scope.importFile == null) { + return; + } + + Admin.importData($scope.importFile, $scope.overrideConfiguration, function() {}, importLavagna, importLavagnaError); + $scope.importing = true; + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin-manage-anonymous-users-access.js b/src/main/webapp/app/controllers/admin/admin-manage-anonymous-users-access.js new file mode 100644 index 000000000..17f12cd4f --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-manage-anonymous-users-access.js @@ -0,0 +1,11 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminManageAnonymousUsersAccess', function ($scope, User, Admin, Permission) { + + + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin-manage-smtp-configuration.js b/src/main/webapp/app/controllers/admin/admin-manage-smtp-configuration.js new file mode 100644 index 000000000..c64436d52 --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-manage-smtp-configuration.js @@ -0,0 +1,71 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminManageSmtpConfigurationCtrl', function ($scope, $http, $modal, Admin) { + + var loadConfiguration = function () { + Admin.findByKey('SMTP_ENABLED').then(function (enabled) { + $scope.smtpEnabled = enabled.second ? JSON.parse(enabled.second) : false; + }); + Admin.findByKey('SMTP_CONFIG').then(function (configuration) { + $scope.configuration = configuration.second ? JSON.parse(configuration.second) : {}; + }); + }; + + loadConfiguration(); + + $scope.$watch('smtpEnabled', function(newVal, oldVal) { + if(newVal == undefined || oldVal == undefined || newVal == oldVal) { + return; + } + Admin.updateConfiguration( + {toUpdateOrCreate: + [{first: 'SMTP_ENABLED', second: newVal}]} + ).catch(function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-manage-smtp-configuration.updateConfiguration.error' + }, false); + }).then(loadConfiguration); + }); + + $scope.saveSmtpConfig = function (conf) { + Admin.updateConfiguration({toUpdateOrCreate: [ + {first: 'SMTP_CONFIG', second: JSON.stringify(conf)} + ]}).then(function() { + Notification.addAutoAckNotification('success', { + key: 'notification.admin-manage-smtp-configuration.saveSmtpConfig.success' + }, false); + }, function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-manage-smtp-configuration.saveSmtpConfig.error' + }, false); + }).then(loadConfiguration); + }; + + $scope.openSmtpConfigModal = function() { + var modalInstance = $modal.open({ + templateUrl: 'partials/admin/fragments/smtp-check-modal.html', + controller: function($scope, $modalInstance, configuration) { + + $scope.sendTestEmail = function (conf, to) { + return $http.post('api/check-smtp/', conf, {params: {to: to}}); + }; + + $scope.close = function() { + $modalInstance.close('done'); + } + }, + size: 'sm', + windowClass: 'lavagna-modal', + resolve: { + configuration: function () { + return $scope.configuration; + } + } + }); + } + }) +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin-manage-users.js b/src/main/webapp/app/controllers/admin/admin-manage-users.js new file mode 100644 index 000000000..6cede62af --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-manage-users.js @@ -0,0 +1,115 @@ +(function() { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminManageUsersCtrl', function($scope, $log, User, UsersAdministration, Admin, Permission, Notification) { + + function loadUsers() { + User.list().then(function(l) { + $scope.users = l; + }); + } + + function loadRoles() { + Permission.findAllRolesAndRelatedPermissions().then(function(res) { + var roleNames = []; + for(var roleName in res) { + if(!res[roleName].hidden) { + roleNames.push(roleName); + } + } + + $scope.roles = roleNames; + }); + } + + function configureDefaultUserToAdd() { + $scope.userToAdd = { + provider: $scope.currentUser != undefined ? $scope.currentUser.provider : null, + username: null, + email: null, + displayName: null, + enabled:true, + roles: { + 'DEFAULT' : true + } + }; + } + + User.currentCachedUser().then(function(u) { + $scope.currentUser = u; + $scope.userToAdd.provider = u.provider; + }); + + configureDefaultUserToAdd(); + + $scope.loginProviders = ["demo", "ldap", "persona", "oauth.bitbucket", "oauth.google", "oauth.github", "oauth.twitter"]; + + loadUsers(); + loadRoles(); + + $scope.updateUserStatus = function(userId, enabled) { + UsersAdministration.toggle(userId, enabled).then(loadUsers, function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-manage-users.toggle.error' + }, false); + }); + }; + + $scope.addUser = function(userToAdd) { + var rawRoles = userToAdd.roles; + var roles = []; + for(var r in rawRoles) { + if(rawRoles[r]) { + roles.push(r); + } + } + userToAdd.roles = roles; + UsersAdministration.addUser(userToAdd).then(function(data) { + configureDefaultUserToAdd(); + loadUsers(); + }, function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-manage-users.add.error' + }, false); + }); + }; + + $scope.importUserFile = null; + + var clearBulkImport = function() { + $scope.importUserFile = null; + }; + + $scope.onFileSelect = function($files) { + $scope.importUserFile = $files[0]; //single file + } + + //TODO: progress bar, abort + $scope.bulkImport = function() { + Admin.importUsers($scope.importUserFile, + function(evt) { $log.debug('percent: ' + parseInt(100.0 * evt.loaded / evt.total)); }, + function(data) { + Notification.addAutoAckNotification('success', { + key: 'notification.admin-manage-users.bulkImport.success' + }, false); + }, + function() { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-manage-users.bulkImport.error' + }, false); + }, + function() { $log.debug('aborted'); }).then(loadUsers); + }; + + // ------- user pagination + $scope.userListPage = 1; + + $scope.switchPage = function(page) { + $scope.userListPage = page; + }; + }); + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin-parameters.js b/src/main/webapp/app/controllers/admin/admin-parameters.js new file mode 100644 index 000000000..9989453ff --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin-parameters.js @@ -0,0 +1,55 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminParametersCtrl', function ($scope, Admin, Notification) { + + + var loadAll = function() { + $scope.configurable = {}; + var configurableKeys = ['TRELLO_API_KEY', 'MAX_UPLOAD_FILE_SIZE', 'USE_HTTPS']; + angular.forEach(configurableKeys, function(v) { + Admin.findByKey(v).then(function(res) { + $scope.configurable[res.first] = res.second; + }); + }); + + Admin.findAllConfiguration().then(function (res) { + $scope.conf = res; + }); + }; + + loadAll(); + + $scope.updateConfiguration = function(k,v) { + Admin.updateKeyConfiguration(k,v).then(function() { + Notification.addAutoAckNotification('success', { + key: 'notification.admin-parameters.updateConfiguration.success', + parameters: { parameterName : k } + }, false); + }, function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-parameters.updateConfiguration.error', + parameters: { parameterName : k } + }, false); + }).then(loadAll) + }; + + $scope.deleteConfiguration = function(k) { + Admin.deleteKeyConfiguration(k).then(function() { + Notification.addAutoAckNotification('success', { + key: 'notification.admin-parameters.deleteConfiguration.success', + parameters: { parameterName : k } + }, false); + }, function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.admin-parameters.deleteConfiguration.error', + parameters: { parameterName : k } + }, false); + }).then(loadAll) + }; + + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/admin/admin.js b/src/main/webapp/app/controllers/admin/admin.js new file mode 100644 index 000000000..f93a1525b --- /dev/null +++ b/src/main/webapp/app/controllers/admin/admin.js @@ -0,0 +1,14 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('AdminCtrl', function ($state, $scope, $rootScope, Admin) { + + Admin.checkHttpsConfiguration().then(function (res) { + $scope.httpsConfigurationCheck = res; + }); + + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/board.js b/src/main/webapp/app/controllers/board.js new file mode 100644 index 000000000..935f7fab5 --- /dev/null +++ b/src/main/webapp/app/controllers/board.js @@ -0,0 +1,458 @@ +(function() { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('ColumnCtrl', function($stateParams, $scope, $filter, $modal, Card, Label, StompClient) { + + $scope.initializeColumnCtrl = function(columnId) { + + var loadCards = function() { + Card.findByColumn(columnId).then(function(res) { + $("[data-lvg-column-id="+columnId+"] .lavagna-to-be-cleaned-up").remove(); + $scope.cardsInColumn = res; + $scope.loaded = true; + $scope.foundCards[columnId] = res; + }); + }; + StompClient.subscribe($scope, '/event/column/'+columnId+'/card', loadCards); + + $scope.$on('loadcards', loadCards); + + loadCards(); + + $scope.$on('$destroy', function() { + delete $scope.foundCards[columnId]; + }); + }; + + $scope.selectAllInColumn = function() { + angular.forEach($filter('filter')($scope.cardsInColumn, $scope.cardFilter), function(c) { + $scope.selectedCards[c.id] = true; + }) + }; + $scope.unSelectAllInColumn = function() { + angular.forEach($filter('filter')($scope.cardsInColumn, $scope.cardFilter), function(c) { + delete $scope.selectedCards[c.id]; + }); + }; + $scope.$on('selectall', $scope.selectAllInColumn); + $scope.$on('unselectall', $scope.unSelectAllInColumn); + + $scope.hasUserLabels = function(cardLabels) { + if (cardLabels === undefined || cardLabels.length === 0) { + return false; //empty, no labels at all + } + for(var i = 0; i < cardLabels.length; i++) { + if(cardLabels[i].labelDomain === 'USER') { + return true; + } + } + return false; + }; + + $scope.hasSystemLabelByName = function(labelName, cardLabels) { + if (cardLabels === undefined || cardLabels.length === 0) + return false; //empty, no labels at all + for(var i = 0; i < cardLabels.length; i++) { + if(cardLabels[i].labelName == labelName && cardLabels[i].labelDomain === 'SYSTEM') { + return true; + } + } + return false; + }; + + $scope.isSelfWatching = function(cardLabels, userId) { + return Card.isWatchedByUser(cardLabels, userId) + }; + + $scope.isAssignedToCard = function(cardLabels, userId) { + return Card.isAssignedToUser(cardLabels, userId); + }; + + + $scope.addUserLabelValue = function(cardId, labelId, user) { + Label.addValueToCard(cardId, labelId, Label.userVal(user)); + }; + + $scope.removeLabelValueForId = function(cardId, cardLabels, labelId) { + for(var i = 0; i < cardLabels.length; i++) { + if(cardLabels[i].labelId == labelId) { + Label.removeValue(cardId, cardLabels[i].labelValueId); + break; + } + } + }; + + $scope.moveAllCardsInColumn = function (col, cards, location) { + + var cardIds = cards.map(function(c) {return c.id}); + var confirmAction = function() {Card.moveAllFromColumnToLocation(col.id, cardIds, location);}; + + $modal.open({ + templateUrl: 'partials/fragments/confirm-modal-fragment.html', + controller: function($scope) { + $scope.operation = $filter('translate')('partials.fragments.confirm-modal-fragment.operation.move-card-from-column-to-location', {columnName: col.name, location: $filter('capitalize')(location)}); + + $scope.confirm = function() { + confirmAction(); + $scope.$close(); + }; + }, + size: 'lg' + }); + } + + var columnScope = $scope; + + $scope.showCardModal = function(boardName, card, isWatching, isAssigned) { + $modal.open({ + templateUrl : 'partials/fragments/card-menu.html', + size:'lg', + controller: function($scope) { + + // + $scope.moveCard = columnScope.moveCard; + $scope.addUserLabelValue = columnScope.addUserLabelValue; + $scope.removeLabelValueForId = columnScope.removeLabelValueForId; + $scope.labelNameToId = columnScope.labelNameToId; + $scope.currentUserId = columnScope.currentUserId; + $scope.moveCardToColumn = columnScope.moveCardToColumn; + // + + $scope.boardName = boardName; + $scope.card = card; + $scope.isWatching = isWatching; + $scope.isAssigned = isAssigned; + } + }); + }; + + }); + + module.controller('BoardCtrl', function($rootScope, $stateParams, $scope, $location, $filter, $log, $timeout, $http, $modal, + Board, Card, Project, LabelCache, Label, Search, StompClient, User, Notification,// + project, board //resolved by ui-router + ) { + + $scope.project = project; + $scope.board = board; + + var boardName = $stateParams.shortName; + $scope.projectName = $stateParams.projectName; + var projectName = $stateParams.projectName; + + + //TODO check redundancy + $scope.boardName = boardName; + $scope.boardShortName = boardName; + + + + User.currentCachedUser().then(function(currentUser) { + $scope.currentUserId = currentUser.id; + }); + + $scope.moveCard = function(card, location) { + Card.moveAllFromColumnToLocation(card.columnId, [card.id], location); + }; + + $scope.backFromLocation = function() { + $scope.locationOpened=false; + $scope.sideBarLocation=undefined; + }; + + $scope.labelNameToId = {}; + var loadLabel = function() { + LabelCache.findByProjectShortName(projectName).then(function(labels) { + $scope.labelNameToId = {}; + for(var k in labels) { + $scope.labelNameToId[labels[k].name] = k; + } + }); + }; + loadLabel(); + + + var unbind = $rootScope.$on('refreshLabelCache-' + projectName, function() { + loadLabel(); + $scope.$broadcast('loadcards'); + }); + $scope.$on('$destroy', unbind); + + //--------------------------------------------------------------- NOTIFICATIONS + + $scope.notifications = Notification.notifications; + + //keep track of the selected cards + $scope.selectedCards = {}; + $scope.foundCards = {}; + + $scope.editMode = false; + $scope.switchEditMode = function() { + $scope.editMode = !$scope.editMode; + //$scope.$apply(function() { + + //}); + }; + + + $scope.selectAll = function() { + $scope.$broadcast('selectall'); + }; + + $scope.unSelectAll = function() { + $scope.$broadcast('unselectall'); + }; + + var selectedVisibleCardsId = function() { + var ids = []; + for(var columnId in $scope.foundCards) { + if($scope.foundCards[columnId]) { + angular.forEach($filter('filter')($scope.foundCards[columnId], $scope.cardFilter), function(c) { + if($scope.selectedCards[c.id]) { + ids.push(c.id); + } + }); + } + } + return ids; + }; + + var selectedVisibleCardsIdByColumnId = function() { + var res = {}; + for(var columnId in $scope.foundCards) { + if($scope.foundCards[columnId]) { + angular.forEach($filter('filter')($scope.foundCards[columnId], $scope.cardFilter), function(c) { + if($scope.selectedCards[c.id]) { + if(!res[c.columnId]) { + res[c.columnId] = []; + } + res[c.columnId].push(c.id); + } + }); + } + } + return res; + }; + + $scope.selectedVisibleCount = function() { + return selectedVisibleCardsId().length; + }; + + + $scope.$on('refreshSearch', function(ev, searchFilter) { + User.currentCachedUser().then(function(user) { + try { + Search.buildSearchFilter(searchFilter.searchFilter, $scope.columns, user.id).then(function(filterFun) { + $scope.cardFilter = filterFun; + $timeout(function() { + $location.search(searchFilter.location); + }); + }); + + } catch(e) { + $log.debug('parsing exception', e); + } + }); + }); + // + + //----------------------------------------------------------------------------------------------------- + //--- TODO cleanup + + // + + Project.columnsDefinition(projectName).then(function(definitions) { + $scope.columnsDefinition = definitions; + }); + // + + $scope.hasMetadata = function(card) { + if(card.counts == null) + return false; //empty + return card.counts['COMMENT'] != undefined || card.counts['FILE'] != undefined + || card.counts['ACTION_CHECKED'] != undefined || card.counts['ACTION_UNCHECKED'] != undefined; + }; + + + $scope.isEmpty = function(obj) { + return Object.keys(obj).length === 0; + }; + + //---------- + $scope.columnsLocation = 'BOARD'; + + var assignToColumn = function(columns) { + $(".lavagna-to-be-cleaned-up").remove(); + $scope.columns = columns; + $rootScope.$broadcast('requestSearch'); + }; + + StompClient.subscribe($scope, '/event/board/'+boardName+'/location/BOARD/column', function() { + Board.columns(boardName, $scope.columnsLocation).then(assignToColumn); + }); + + Board.columns(boardName, 'BOARD').then(assignToColumn); + + //------------- + + User.hasPermission('MOVE_COLUMN', $stateParams.projectName).then(function() { + $scope.sortableColumnOptions = { + tolerance: "pointer", + handle: '.lvg-column-handle', + forceHelperSize: true, + items: '> .lavagna-sortable-board-column', + cancel: 'input,textarea,button,select,option,.lavagna-not-sortable-board-column,.lvg-board-column-menu', + placeholder: "lavagna-column-placeholder", + start : function(e, ui) { + ui.placeholder.height(ui.helper.outerHeight()); + }, + stop: function(e, ui) { + if(ui.item.data('hasUpdate')) { + var colPos = ui.item.parent().sortable("toArray", {attribute: 'data-lvg-column-id'}).map(function(i) {return parseInt(i, 10);}); + //mark the placeholder item + //ui.item.addClass('lavagna-to-be-cleaned-up'); + Board.reorderColumn(boardName, $scope.columnsLocation, colPos); + } + ui.item.removeData('hasUpdate'); + }, + update : function(e, ui) { + ui.item.data('hasUpdate', true); + } + }; + }, function() { + $scope.sortableColumnOptions = false; + }); + + + User.hasPermission('MOVE_CARD', $stateParams.projectName).then(function() { + $scope.sortableCardOptions = { + tolerance: "pointer", + connectWith: ".lavagna-board-cards,.sidebar-dropzone", + cancel: '.lvg-not-sortable-card', + placeholder: "lavagna-card-placeholder", + start : function(e, ui) { + $("#sidebar-drop-zone").show(); + ui.placeholder.height(ui.helper.outerHeight()); + ui.item.data('initialColumnId', ui.item.parent().parent().parent().attr('data-lvg-column-id')); + }, + stop: function(e, ui) { + $("#sidebar-drop-zone").hide(); + if(ui.item.data('hasUpdate')) { + var cardId = parseInt(ui.item.attr('data-lvg-card-id'), 10); + var oldColumnId = parseInt(ui.item.data('initialColumnId'), 10); + var newColumnId = parseInt(ui.item.data('newColumnId'), 10); + var ids = ui.item.parent().sortable("toArray", {attribute: 'data-lvg-card-id'}).map(function(i) {return parseInt(i, 10);}); + + ui.item.addClass('lavagna-to-be-cleaned-up'); + ui.item.replaceWith(ui.item.clone()); + + if(oldColumnId === newColumnId) { + Board.updateCardOrder(boardName, oldColumnId, ids); + } else { + Board.moveCardToColumn(cardId, oldColumnId, newColumnId, {newContainer: ids}); + } + } + ui.item.removeData('hasUpdate'); + ui.item.removeData('initialColumnId'); + ui.item.removeData('newColumnId'); + }, + update : function(e, ui) { + ui.item.data('newColumnId', ui.item.parent().parent().parent().attr('data-lvg-column-id')); + ui.item.data('hasUpdate', true); + }, + //http://stackoverflow.com/a/20228500 + over: function(e, ui) { + ui.item.data('sortableItem').scrollParent = ui.placeholder.parent(); + ui.item.data('sortableItem').overflowOffset = ui.placeholder.parent().offset(); + } + }; + }, function() { + $scope.sortableCardOptions = false; + }); + + + + + //will be used as a map columnState[columnId].editColumnName = true/false + $scope.columnState = {}; + + $scope.createColumn = function(columnToCreate) { + Board.createColumn(boardName, columnToCreate).then(function() { + columnToCreate.name = null; + columnToCreate.definition = null; + }); + }; + + $scope.createCardFromTop = function(cardToCreateFromTop, column) { + Board.createCardFromTop(boardName, column.id, {name: cardToCreateFromTop.name}).then(function() { + cardToCreateFromTop.name = null; + }); + }; + + $scope.createCard = function(cardToCreate, column) { + Board.createCard(boardName, column.id, {name: cardToCreate.name}).then(function() { + cardToCreate.name = null; + }); + }; + + $scope.saveNewColumnName = function(newName, column) { + Board.renameColumn(boardName, column.id, newName); + }; + + $scope.setColumnDefinition = function(definition, column) { + Board.redefineColumn(boardName, column.id, definition); + }; + + $scope.moveColumn = function(column, location) { + + var confirmAction = function() {Board.moveColumnToLocation(column.id, location);}; + + $modal.open({ + templateUrl: 'partials/fragments/confirm-modal-fragment.html', + controller: function($scope) { + $scope.operation = $filter('translate')('partials.fragments.confirm-modal-fragment.operation.move-column-to-location', {columnName: column.name, location: $filter('capitalize')(location)}); + $scope.confirm = function() { + confirmAction(); + $scope.$close(); + }; + }, + size: 'lg' + }); + + + }; + //----------------------------------------------------------------------------------------------------- + + var formatBulkRequest = function() { + var r = {}; + r[projectName] = selectedVisibleCardsId(); + return r; + }; + + $scope.formatBulkRequest = formatBulkRequest; + $scope.selectedVisibleCardsIdByColumnId = selectedVisibleCardsIdByColumnId; + + //----------------------------------------------------------------------------------------------------- + + //some sidebar controls + $scope.sideBarLocation = undefined; + $scope.toggledSidebar = false; + + $scope.sidebarHandler = function(location) { + $scope.sideBarLocation = location; + + // shall we close the sidebar? + if(location === 'NONE' && $scope.toggledSidebar) { + $scope.toggledSidebar = false; + }; + + // shall we open the sidebar? + if(location !== 'NONE' && !$scope.toggledSidebar) { + $scope.toggledSidebar = true; + }; + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/card.js b/src/main/webapp/app/controllers/card.js new file mode 100644 index 000000000..b6e6ed5c2 --- /dev/null +++ b/src/main/webapp/app/controllers/card.js @@ -0,0 +1,562 @@ +(function() { + + //FIXME, TODO: clean up the label part, it's a wreck + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('CardCtrl', function($stateParams, $scope, $rootScope, $filter, $timeout, + CardCache, Card, User, LabelCache, Label, StompClient, Notification, Board, + card, currentUser, project, board //resolved by ui-router + ) { + + + var findAndAssignColumns = function() { + Board.columns(board.shortName, 'BOARD').then(function(columns) { + $scope.columns = columns; + }); + }; + + StompClient.subscribe($scope, '/event/board/'+board.shortName+'/location/BOARD/column', findAndAssignColumns); + + findAndAssignColumns(); + + $scope.codeMirrorOptions = { + lineWrapping : true, + lineNumbers: true, + mode: 'markdown', + viewportMargin: Infinity + }; + + $scope.currentUser = currentUser; + $scope.project = project; + $scope.board = board; + $scope.card = card; + + // + + $scope.notSameColumn = function(col) { + return col.id != $scope.card.columnId; + }; + + $scope.notEmptyColumn = function(model) { + return model != "" && model != null; + } + // + + var $scopeForColumn = undefined; + + var loadColumn = function(columnId) { + Board.column(columnId).then(function(col) { + $scope.column = col; + }); + + if($scopeForColumn) { + $scopeForColumn.$destroy(); + } + + $scopeForColumn = $scope.$new() + + //subscribe on any change to the column + StompClient.subscribe($scopeForColumn, '/event/column/' + columnId, function() { + loadColumn(columnId); + }); + + }; + loadColumn(card.columnId); + // + + var refreshTitle = function() { + $rootScope.pageTitle = '[' + board.shortName + '-' + $scope.card.sequence + '] ' + $scope.card.name + ' - Lavagna'; + }; + refreshTitle(); + //------------------ + + var reloadCard = function() { + CardCache.card(card.id).then(function(c) { + $scope.card = c; + refreshTitle(); + loadColumn(c.columnId); + }); + loadActivity(); + }; + + var unbindCardCache = $rootScope.$on('refreshCardCache-' + card.id, reloadCard); + $scope.$on('$destroy', unbindCardCache); + + var loadDescription = function() { + Card.description(card.id).then(function(description) { + $scope.description = description; + }); + }; + + loadDescription(); + + $scope.updateDescription = function(description) { + Card.updateDescription(card.id, description).then(function() { + description.content = null; + }); + }; + + var loadComments = function() { + Card.comments(card.id).then(function(comments) { + $scope.comments = comments; + }); + }; + + loadComments(); + + $scope.labelNameToId = {}; + + var loadLabel = function() { + LabelCache.findByProjectShortName(project.shortName).then(function(labels) { + $scope.labels = labels; + $scope.userLabels = {}; + $scope.labelNameToId = {}; + for(var k in labels) { + $scope.labelNameToId[labels[k].name] = k; + if(labels[k].domain === 'USER') { + $scope.userLabels[k] = labels[k]; + } + } + }); + }; + loadLabel(); + + var unbind = $rootScope.$on('refreshLabelCache-' + project.shortName, loadLabel); + $scope.$on('$destroy', unbind); + + var loadLabelValues = function() { + Label.findValuesByCardId(card.id).then(function(labelValues) { + $scope.labelValues = labelValues; + }); + }; + loadLabelValues(); + + //---------------------- + var loadActionListsAndRefresh = function() { + loadActionLists(); + reloadCard(); + }; + + var loadActionLists = function() { + Card.actionLists(card.id).then(function(actionLists) { + $scope.actionLists = []; + $scope.actionListsById = {}; + $scope.actionListsId = []; + for(var i = 0; i < actionLists.lists.length; i++) { + $scope.actionListsId.push(actionLists.lists[i].id); + $scope.actionListsById[actionLists.lists[i].id] = actionLists.lists[i]; + $scope.actionLists.push(actionLists.lists[i]); + } + + $scope.actionItems = actionLists.items; + $scope.actionItemsMap = {}; + $scope.actionListsStats = {}; + for(var i = 0; i < $scope.actionListsId.length; i++) { + var currentListID = $scope.actionListsId[i]; + //there could be a list without elements + $scope.actionListsStats[currentListID] = 0; + if(actionLists.items[currentListID] === undefined) + continue; + var checkedItems = 0; + for(var e = 0; e < actionLists.items[currentListID].length; e++) { + var item = actionLists.items[currentListID][e]; + if(item.type === 'ACTION_CHECKED') + checkedItems++; + $scope.actionItemsMap[item.id] = item; + } + $scope.actionListsStats[currentListID] = parseInt((checkedItems/actionLists.items[currentListID].length) * 100, 10); + } + }); + }; + loadActionLists(); + + User.hasPermission('ORDER_ACTION_LIST', $stateParams.projectName).then(function() { + $scope.sortableActionListOptions = { + items: '> .lavagna-sortable-card-actionlist', + cancel: 'input,textarea,button,select,option,.lavagna-not-sortable-card-actionlist', + start : function(e, ui) {}, + stop : function(e, ui) {}, + update : function(e, ui) { + var newActionListsId = ui.item.parent().sortable("toArray", {attribute: 'data-lavagna-actionlist-id'}).map(function(i) {return parseInt(i, 10);}); + Card.updateActionListOrder(card.id, newActionListsId); + } + }; + }, function() { + $scope.sortableActionListOptions = false; + }) + + User.hasPermission('MOVE_ACTION_LIST_ITEM', $stateParams.projectName).then(function() { + $scope.sortableActionItemsOptions = { + connectWith: ".lavagna-card-actionitems", + start:function(e, ui) { + ui.item.data('initialActionlistId', ui.item.parent().parent().parent().attr('data-lavagna-actionlist-id')); + }, + stop: function(e, ui) { + if(ui.item.data('hasUpdate')) { + var itemId = parseInt(ui.item.attr('data-lavagna-actionlistitem-id'), 10); + var oldActionlistId = parseInt(ui.item.data('initialActionlistId'), 10); + var newActionlistId = parseInt(ui.item.data('newActionlistId'), 10); + var ids = ui.item.parent().sortable("toArray", {attribute: 'data-lavagna-actionlistitem-id'}).map(function(i) {return parseInt(i, 10);}); + if(oldActionlistId === newActionlistId) { + Card.updateActionItemOrder(oldActionlistId, ids); + } else { + Card.moveActionItem(itemId, newActionlistId, {newContainer: ids}); + } + } + ui.item.removeData('hasUpdate'); + ui.item.removeData('initialActionlistId'); + ui.item.removeData('newActionlistId'); + }, + update : function(e, ui) { + ui.item.data('newActionlistId', ui.item.parent().parent().parent().attr('data-lavagna-actionlist-id')); + ui.item.data('hasUpdate', true); + } + }; + }, function() { + $scope.sortableActionItemsOptions = false; + }); + // + + //-------------- + + $scope.actionListState = {}; + + $scope.addComment = function(comment) { + Card.addComment(card.id, comment).then(function() { + comment.content = null; + }); + }; + + $scope.addActionList = function(actionList) { + Card.addActionList(card.id, actionList).then(function() { + actionList.content = null; + }); + }; + + $scope.deleteActionList = function(itemId) { + Card.deleteActionList(itemId).then(function(data) { + Notification.addNotification('success', { key : 'notification.card.ACTION_LIST_DELETE.success'}, true, function(notification) { + Card.undoDeleteActionList(notification.event.id).then(notification.acknowledge) + }); + }, function(error) { + $scope.actionListState[listId].deleteList = false; + Notification.addAutoAckNotification('error', { key : 'notification.card.ACTION_LIST_DELETE.success'}, false); + }); + }; + + $scope.saveActionList = function(itemId, content) { + Card.updateActionList(itemId, content); + }; + + $scope.addActionItem = function(listId, actionItem) { + Card.addActionItem(listId, actionItem).then(function() { + actionItem.content = null; + }); + }; + + $scope.deleteActionItem = function(listId, itemId) { + Card.deleteActionItem(itemId).then(function(data) { + Notification.addNotification('success', { key : 'notification.card.ACTION_ITEM_DELETE.success'}, true, function(notification) { + Card.undoDeleteActionItem(notification.event.id).then(notification.acknowledge); + }); + }, function(error) { + $scope.actionListState[listId][itemId].deleteActionItem = false; + $scope.actionListState[listId][itemId].showControls = false; + Notification.addAutoAckNotification('error', { key : 'notification.card.ACTION_ITEM_DELETE.error'}, false); + }); + }; + + $scope.toggleActionItem = function(itemId) { + Card.toggleActionItem(itemId, ($scope.actionItemsMap[itemId].type === 'ACTION_UNCHECKED')); + }; + + $scope.saveActionItem = function(itemId, content) { + Card.updateActionItem(itemId, content); + }; + + // ----- + $scope.updateCardName = function(card, newName) { + Card.update(card.id, newName); + }; + // + $scope.updateComment = function(comment, commentToEdit) { + Card.updateComment(comment.id, commentToEdit); + }; + $scope.deleteComment = function(comment, control) { + Card.deleteComment(comment.id).then(function(data) { + Notification.addNotification('success', { key : 'notification.card.COMMENT_DELETE.success'}, true, function(notification) { + Card.undoDeleteComment(notification.event.id).then(notification.acknowledge) + }); + }, function(error) { + control = false; + Notification.addAutoAckNotification('error', { key : 'notification.card.ACTION_ITEM_DELETE.error'}, false); + }); + }; + + // labels + + $scope.hasUserLabels = function(userLabels, labelValues) { + if(userLabels === undefined || labelValues === undefined) { + return false; + } + var count = 0; + for(var n in userLabels) { + count += labelValues[n] === undefined ? 0 : labelValues[n].length; + } + return count > 0; + } + + $scope.removeLabelValue = function(label) { + + LabelCache.findLabelByProjectShortNameAndId(project.shortName, label.labelId).then(function(data) { + return data.domain === 'USER'; + }).then(function(notify) { + //we only notify for USER label removal + Label.removeValue(card.id, label.cardLabelValueId).then(function(data) { + if(notify) { + Notification.addAutoAckNotification('success', { + key : 'notification.card.LABEL_DELETE.success', + parameters : { + labelName : $scope.userLabels[label.labelId].name } + }, false); + } + }, function(error) { + $scope.actionListState[listId].deleteList = false; + if(notify) { + Notification.addAutoAckNotification('error', { + key : 'notification.card.LABEL_DELETE.error', + parameters : { + labelName : $scope.userLabels[label.labelId].name } + }, false); + } + }) + }) + }; + + $scope.findElementWithUserId = function(arr, val) { + if(!angular.isArray(arr)) { + return false; + } + for(var i = 0; i < arr.length; i++) { + if(arr[i].value.valueUser === val) { + return arr[i]; + } + } + return false; + }; + + $scope.updateLabelValue = function(label, prevValues, value) { + var labelValueToUpdate = Label.extractValue(label, value); + if(prevValues === undefined) { + Label.addValueToCard(card.id, label.id, labelValueToUpdate); + } else { + Label.updateValue(card.id, prevValues[0].cardLabelValueId, labelValueToUpdate); + } + }; + + $scope.addNewLabel = function(labelToAdd) { + $scope.updateLabelValue(labelToAdd.label, undefined, labelToAdd.value); + }; + + //-- file upload + var loadFiles = function() { + Card.files(card.id).then(function(files) { + $scope.files = {}; + for(var f = 0; f < files.length; f++) { + var file = files[f]; + $scope.files[file.cardDataId] = file; + } + }); + }; + + loadFiles(); + + var processUploadingFile = function(icon, status) { + $timeout(function() { + $scope.processedFiles.push({'icon': icon, 'status': status, 'file': $scope.uploadingFiles['file']}); + $scope.uploadingFile = null; + }); + }; + + + var fileUploadProgressCallBack = function(event) { + if (event.lengthComputable) { + $scope.uploadingFile['progress'] = Math.round(event.loaded * 100 / event.total); + } + }; + + var fileUploadCompleteCallBack = function(data, status) { + if(status == 200) { + $scope.uploadingFile = null; + } else { + processUploadingFile('warning', 'failed'); + } + + }; + + var fileUploadFailedCallBack = function() { + processUploadingFile('warning', 'failed'); + }; + + var fileUploadCanceledCallBack = function() { + processUploadingFile('stop', 'aborted by user'); + }; + + $scope.filesToUpload = []; + $scope.uploadingFile = null; + $scope.processedFiles = []; + + $scope.addFiles = function($files) { + $scope.$apply(function() { + for (var i = 0; i < $files.length; i++) { + $scope.filesToUpload.push($files[i]); + } + }); + }; + + $scope.uploadNextFile = function() { + $timeout(function() { + Card.getMaxFileSize().then(function(size) { + var file = $scope.filesToUpload.shift(); + var maxSize = size === "" ? Math.NaN : parseInt(JSON.parse(size)); + if(!isNaN(maxSize) && file.size > maxSize) { + Notification.addNotification('error', {key : 'notification.error.file-size-too-big', parameters: {maxSize :maxSize}}, false); + return; + } + + $scope.uploadingFile = { + 'xhr': Card.uploadFile(file, card.id, fileUploadProgressCallBack, fileUploadCompleteCallBack, + fileUploadFailedCallBack, fileUploadCanceledCallBack), + 'progress': 0, + 'file' : file + }; + }); + }); + }; + + $scope.abortUpload = function() { + $scope.uploadingFile['xhr'].abort(); + }; + + $scope.cancelUpload = function(elementIndex) { + $scope.processedFiles.push({'icon': 'stop', 'status': 'aborted by user', 'file': $scope.filesToUpload.splice(elementIndex, 1)[0]}); + }; + + $scope.clearFileUploadResultQueue = function() { + $scope.processedFiles = []; + }; + + + $scope.retryUpload = function(elementIndex) { + $timeout(function() { + $scope.$apply(function() { + $scope.filesToUpload.push($scope.processedFiles.splice(elementIndex, 1)[0]['file']); + }); + }); + }; + + $scope.deleteFile = function(dataId) { + Card.deleteFile(dataId).then(function(data) { + Notification.addNotification('success', {key : 'notification.card.FILE_DELETE.success'}, true, function(notification) { + Card.undoDeleteFile(notification.event.id).then(notification.acknowledge); + }); + }, function(error) { + $scope.fileControls[dataId].deleteFile = false; + Notification.addAutoAckNotification('error', {key : 'notification.card.FILE_DELETE.error'}, false); + }); + }; + + $scope.$watchCollection('uploadingFile', function() { + if($scope.filesToUpload.length > 0 && $scope.uploadingFile == null) { + $scope.uploadNextFile(); + } + }); + + $scope.$watchCollection('filesToUpload', function() { + if($scope.filesToUpload.length > 0 && $scope.uploadingFile == null) { + $scope.uploadNextFile(); + } + }); + + //activity + var loadActivity = function() { + Card.activity(card.id).then(function(activities) { + $scope.activities = activities; + }); + }; + + loadActivity(); + + $scope.hasActivity = function() { + return ($scope.comments !== undefined && $scope.actionLists !== undefined && $scope.actionItems !== undefined && $scope.files !== undefined); + }; + + $scope.moveCard = function(card, location) { + Card.moveAllFromColumnToLocation(card.columnId, [card.id], location).then(reloadCard).then(function() { + Notification.addAutoAckNotification('success', { + key: 'notification.card.moveToLocation.success', + parameters: { location: location } + }, false); + }, function(error) { + Notification.addAutoAckNotification('error', { + key: 'notification.card.moveToLocation.error', + parameters: { location: location } + }, false); + }); + }; + + $scope.moveToColumn = function(card, toColumn) { + if(angular.isUndefined(toColumn)) { + return; + } + + Card.findByColumn(toColumn.id).then(function(cards) { + var ids = []; + for (var i = 0;i -1) { + loadLabelValues(); + reloadCard(); + } else if(type.match(/FILE$/g)) { + loadFiles(); + reloadCard(); + } + }); + }); + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/error.js b/src/main/webapp/app/controllers/error.js new file mode 100644 index 000000000..af901e4dd --- /dev/null +++ b/src/main/webapp/app/controllers/error.js @@ -0,0 +1,13 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('ErrorCtrl', function ($stateParams, $scope) { + $scope.parentResource = { + url: decodeURIComponent($stateParams.resourceId), + show: ($stateParams.resourceId !== '') + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/home.js b/src/main/webapp/app/controllers/home.js new file mode 100644 index 000000000..cf260b3a7 --- /dev/null +++ b/src/main/webapp/app/controllers/home.js @@ -0,0 +1,81 @@ +(function() { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('HomeCtrl', function($scope, Project, User, Notification, StompClient) { + + $scope.projectPage = 1; + $scope.projectsPerPage = 10; + $scope.maxVisibleProjectPages = 3; + + var loadProjects = function() { + Project.list().then(function(projects) { + $scope.projects = projects; + $scope.totalProjects = projects.length; + + $scope.switchProjectPage($scope.projectPage); + }); + }; + + loadProjects(); + + $scope.switchProjectPage = function(page) { + $scope.currentProjects = $scope.projects.slice((page - 1) * $scope.projectsPerPage, + ((page - 1) * $scope.projectsPerPage) + $scope.projectsPerPage); + }; + + $scope.cardPage = 1; + $scope.maxVisibleCardPages = 3; + + var loadUserCards = function(page) { + User.isAuthenticated().then(function() {return User.hasPermission('SEARCH')}).then(function() { + User.cards(page).then(function(cards) { + $scope.userCards = cards.cards; + $scope.totalOpenCards = cards.totalCards; + $scope.cardsPerPage = cards.itemsPerPage; + }); + }); + }; + + loadUserCards($scope.cardPage - 1); + + $scope.fetchUserCardsPage = function(page) { + loadUserCards(page - 1); + }; + + StompClient.subscribe($scope, '/event/project', loadProjects); + + $scope.suggestProjectShortName = function(project) { + if(project == null ||project.name == null || project.name == "") { + return; + } + Project.suggestShortName(project.name).then(function(res) { + project.shortName = res.suggestion; + }); + }; + + $scope.$watch('project.shortName', function(newVal) { + if(newVal !== undefined && newVal !== null) { + Project.checkShortName(newVal).then(function(res) { + $scope.checkedShortName = res; + }); + } else { + $scope.checkedShortName = undefined; + } + }); + + + $scope.createProject = function(project) { + project.shortName = project.shortName.toUpperCase(); + Project.create(project).then(function() { + $scope.project = {}; + $scope.checkedShortName = undefined; + Notification.addAutoAckNotification('success', {key: 'notification.project.creation.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', {key: 'notification.project.creation.error'}, false); + }); + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/manage-role.js b/src/main/webapp/app/controllers/manage-role.js new file mode 100644 index 000000000..5fa10314c --- /dev/null +++ b/src/main/webapp/app/controllers/manage-role.js @@ -0,0 +1,257 @@ +(function() { + + //shared by admin and project admin + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('ManageRoleCtrl', function($scope, $stateParams, Permission, Notification, ProjectCache, StompClient, $modal, $filter) { + + //handle manage role at project level: + var projectName = undefined; + if($stateParams.projectName !== undefined) { + projectName = $stateParams.projectName; + ProjectCache.project(projectName).then(function(res) { + $scope.project = res; + }); + Permission = Permission.forProject(projectName); + } + + var reloadRoles = function() { + Permission.findAllRolesAndRelatedPermissions().then(function(res) { + + for(var roleName in res) { + if(res[roleName].hidden) { + delete res[roleName]; + } + } + + $scope.roles = res; + for(var roleName in res) { + $scope.loadUsersWithRole(roleName); + } + }); + }; + + var reloadUserWithRole = function(roleName) { + if(angular.isDefined($scope.usersWithRole[roleName])) { + $scope.loadUsersWithRole(roleName); + } + }; + + var route = '/event/permission' + (projectName === undefined ? '' : ('/project/'+ projectName)); + StompClient.subscribe($scope, route , function(event) { + var b = JSON.parse(event.body); + if(b.type === 'REMOVE_ROLE_TO_USERS' || b.type === 'ASSIGN_ROLE_TO_USERS') { + reloadUserWithRole(b.payload); + } else { + reloadRoles(); + } + }); + + $scope.usersWithRole = {}; + + $scope.loadUsersWithRole = function(roleName) { + Permission.findUsersWithRole(roleName).then(function(users) { + $scope.usersWithRole[roleName] = users; + }); + }; + + $scope.roleState = {}; + + $scope.createRole = function(roleName) { + Permission.createRole(roleName).catch(function() { + Notification.addAutoAckNotification('error', { + key: 'notification.manage-role.createRole.error' + }, false); + }); + }; + + $scope.deleteRole = function(roleName) { + Permission.deleteRole(roleName).catch(function() { + Notification.addAutoAckNotification('error', { + key: 'notification.manage-role.deleteRole.error', + parameters: {roleName : roleName} + }, false); + }); + }; + + var isRoleAssignedToUser = function(userId, roleName) { + for(var i = 0; i < $scope.usersWithRole[roleName].length; i++ ) { + if($scope.usersWithRole[roleName][i].id == userId) { + return true; + } + } + return false; + } + + $scope.addUserToRole = function(username, roleName) { + if(!isRoleAssignedToUser(username.id, roleName)) { + Permission.addUserToRole(username.id, roleName).catch(function() { + Notification.addAutoAckNotification('error', { + key: 'notification.manage-role.addUserToRole.error', + parameters: {roleName : roleName, userName : $filter('formatUser')(username)} + }, false); + }); + } + }; + + $scope.removeUserToRole = function(username, roleName) { + Permission.removeUserToRole(username, roleName).catch(function() { + Notification.addAutoAckNotification('error', { + key: 'notification.manage-role.removeUserToRole.error', + parameters: {roleName : roleName, userName : $filter('formatUser')(username)} + }, false); + }); + }; + + reloadRoles(); + + Permission.allAvailablePermissionsByCategory().then(function(res) { + $scope.permissionsByCategory = res; + }); + + $scope.userListPage = 1; + $scope.switchUserPage = function(page) { + $scope.userListPage = page; + }; + + //modal + $scope.open = function (roleName, roleDescriptor) { + + var modalInstance = $modal.open({ + templateUrl: 'partials/fragments/common-manage-roles-modal.html', + controller: ModalInstanceCtrl, + size: 'lg', + windowClass: 'lavagna-modal', + keyboard: false, + backdrop: 'static', + resolve: { + role: function () { + return roleName; + }, + roleDescriptor : function() { + return roleDescriptor; + }, + permissionsByCategory : function() { + return $scope.permissionsByCategory; + } + } + }); + + }; + + var ModalInstanceCtrl = function ($scope, $modalInstance, role, roleDescriptor, permissionsByCategory) { + + $scope.roleName = role; + $scope.roleDescriptor = roleDescriptor; + $scope.permissionsByCategory = permissionsByCategory; + + $scope.save = function() { + var permissionsToEnable = []; + angular.forEach($scope.assignStatus, function(value, key) { + if(value.checked) { + permissionsToEnable.push(key); + } + }); + Permission.updateRole(role, permissionsToEnable).catch(function() { + Notification.addAutoAckNotification('error', { + key: 'notification.manage-role.updateRole.error', + parameters: {roleName : role} + }, false); + }).then($scope.cancel); + }; + + var hasChanges = function() { + var result = false; + + //perhaps slower than foreach probably, but avoid traversing the entire object + for(var key in $scope.assignStatus) { + var value = $scope.assignStatus[key]; + var change = (value.checked != $scope.hasPermission(key, $scope.roleDescriptor.roleAndPermissions)); + if(change) { return true; } + } + return false; + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + + $scope.cancelWithConfirmation = function() { + if(!hasChanges()) { + $scope.cancel(); + return; + } + + var modal = $modal.open({ + templateUrl: 'partials/fragments/common-manage-roles-modal-confirmation.html', + controller: function($scope, $modalInstance, roleName) { + $scope.roleName = roleName; + + $scope.confirm = function() { + $modalInstance.close('save'); + } + + $scope.deny = function() { + $modalInstance.close('notsave'); + } + + $scope.cancel = function() { + $modalInstance.close('cancel'); + } + }, + windowClass: 'lavagna-modal', + size: 'sm', + keyboard: false, + resolve: { + roleName: function() { + return $scope.roleName; + } + } + }); + + modal.result.then(function(result) { + if(result === 'save') { + $scope.save(); + } + + if(result === 'notsave') { + $scope.cancel(); + } + }) + } + + $scope.assignStatus = {}; + + $scope.hasChanged = function(permission, assignedPermissions, currentStatus) { + var status = $scope.hasPermission(permission, assignedPermissions); + return status != currentStatus; + } + + /* TODO could remove the linear probe... */ + $scope.hasPermission = function(permission, assignedPermissions) { + if(permission == undefined || assignedPermissions == undefined) { + return; + } + + for(var i = 0; i minLength; + }; + + var orderByStatus = function (milestone) { + var insertStatusIfExists = function (milestone, source, target, status) { + if (source[status] != undefined) { + target[target.length] = {status: status, count: source[status]}; + milestone.totalCards += source[status]; + } + }; + + milestone.totalCards = 0; + var sorted = []; + insertStatusIfExists(milestone, milestone.cardsCountByStatus, sorted, "BACKLOG"); + insertStatusIfExists(milestone, milestone.cardsCountByStatus, sorted, "OPEN"); + insertStatusIfExists(milestone, milestone.cardsCountByStatus, sorted, "DEFERRED"); + insertStatusIfExists(milestone, milestone.cardsCountByStatus, sorted, "CLOSED"); + $scope.cardsCountByStatus[milestone.labelListValue.value] = sorted; + }; + + $scope.moveDetailToPage = function (milestone, page) { + User.hasPermission('READ', $stateParams.projectName).then(function () { + return Card.findCardsByMilestoneDetail($stateParams.projectName, milestone.labelListValue.value, page); + }).then(function (response) { + milestone.detail = response; + milestone.currentPage = response.cards.currentPage + 1; + }); + }; + + var loadMilestonesInProject = function () { + User.hasPermission('READ', $stateParams.projectName).then(function () { + return Card.findCardsByMilestone($stateParams.projectName); + }).then(function (response) { + $scope.cardsByMilestone = response.milestones; + $scope.cardsCountByStatus = []; + for (var index in response.milestones) { + var milestone = response.milestones[index]; + orderByStatus(milestone); + if ($scope.milestoneOpenStatus[milestone.labelListValue.value]) { + $scope.moveDetailToPage(milestone, 0); + } + } + $scope.statusColors = response.statusColors; + }); + }; + + loadMilestonesInProject(); + + StompClient.subscribe($scope, '/event/project/' + $stateParams.projectName + '/label-value', loadMilestonesInProject); + + StompClient.subscribe($scope, '/event/project/' + $stateParams.projectName + '/label', loadMilestonesInProject); + + $scope.clearMilestoneDetail = function (milestone) { + milestone.detail = null; + milestone.currentPage = 1; + }; + + $scope.loadMilestoneDetail = function (milestone) { + $scope.moveDetailToPage(milestone, 0); + }; + + $scope.toggleMilestoneOpenStatus = function (milestone) { + var currentOpenStatus = $scope.milestoneOpenStatus[milestone.labelListValue.value]; + currentOpenStatus ? $scope.clearMilestoneDetail(milestone) : $scope.loadMilestoneDetail(milestone); + $scope.milestoneOpenStatus[milestone.labelListValue.value] = !currentOpenStatus; + }; + + $scope.updateMilestone = function (milestone, newName) { + var newLabelValue = jQuery.extend({}, milestone.labelListValue); + newLabelValue.value = newName; + Label.updateLabelListValue(newLabelValue); + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/project/project-statistics.js b/src/main/webapp/app/controllers/project/project-statistics.js new file mode 100644 index 000000000..e61944a87 --- /dev/null +++ b/src/main/webapp/app/controllers/project/project-statistics.js @@ -0,0 +1,150 @@ +(function () { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('ProjectStatisticsCtrl', function ($stateParams, $scope, $translate, $filter, Project, Board, project //resolved by ui-route + ) { + + $scope.sidebarOpen = true; + $scope.project = project; + + + $scope.showCreatedAndClosedCards = false; + $scope.showCardsHistory = false; + $scope.showCardsByLabel = false; + + $scope.cardsHistoryChartOptions = { + pointDot: false, + bezierCurve: false, + scaleIntegersOnly: true, + showTooltips: false, + scaleBeginAtZero: true, + responsive: true, + maintainAspectRatio: false + }; + + $scope.cardsByLabelChartOptions = { + responsive: true, + maintainAspectRatio: false + }; + + var generateDashboard = function (stats) { + var cardsHistory = { + labels: [], + datasets: [] + }; + + function getSortedIndexes(array) { + var sortedIndexes = []; + for (var index in array) { + sortedIndexes.push(parseInt(index)); + } + sortedIndexes.sort(); + return sortedIndexes; + } + + function getStatusFromHistory(stats, index, definition) { + return stats.statusHistory[index][definition] || 0; + } + + function createCardsHistorySeries(label, color) { + cardsHistory.datasets.push({label: label, strokeColor: color, fillColor: color, pointColor: color, data: []}); + } + + createCardsHistorySeries("Backlog", $filter('color')(stats.backlogTaskColor).color); + createCardsHistorySeries("Open", $filter('color')(stats.openTaskColor).color); + createCardsHistorySeries("Deferred", $filter('color')(stats.deferredTaskColor).color); + createCardsHistorySeries("Closed", $filter('color')(stats.closedTaskColor).color); + + + var sortedHistoryIndexes = getSortedIndexes(stats.statusHistory); + + // cards history + for (var i = 0; i < sortedHistoryIndexes.length; i++) { + var index = sortedHistoryIndexes[i]; + cardsHistory.labels.push(new Date(index).toLocaleDateString()); + + var closed = getStatusFromHistory(stats, index, "CLOSED"); + cardsHistory.datasets[3].data.push(closed); + + var deferred = closed + getStatusFromHistory(stats, index, "DEFERRED"); + cardsHistory.datasets[2].data.push(deferred); + + var open = deferred + getStatusFromHistory(stats, index, "OPEN"); + cardsHistory.datasets[1].data.push(open); + + var backlog = open + getStatusFromHistory(stats, index, "BACKLOG"); + cardsHistory.datasets[0].data.push(backlog); + } + + $scope.stats = stats; + + $scope.totalCards = stats.openTaskCount + stats.closedTaskCount + stats.deferredTaskCount + stats.backlogTaskCount; + + $scope.chartCardsHistoryData = cardsHistory; + $scope.showCardsHistory = cardsHistory.labels.length > 1; + + // cards by label + $scope.cardsByLabelMax = 1; + for (var i = 0; i < stats.cardsByLabel.length; i++) { + var label = stats.cardsByLabel[i]; + $scope.cardsByLabelMax = Math.max($scope.cardsByLabelMax, label.count); + } + $scope.showCardsByLabel = stats.cardsByLabel.length > 0; + + // created vs closed + $scope.openedThisPeriod = 0; + $scope.closedThisPeriod = 0; + + for (var index in stats.createdAndClosedCards) { + var pair = stats.createdAndClosedCards[index]; + $scope.openedThisPeriod += pair['first']; + $scope.closedThisPeriod += pair['second']; + } + $scope.showCreatedAndClosedCards = Object.keys(stats.createdAndClosedCards).length > 1; + }; + + $scope.availableDateRanges = []; + + $translate('partials.project.statistics.last30Days').then(function (translatedValue) { + $scope.availableDateRanges.push({name: translatedValue, value: moment().day(-30).toDate()}); + }); + $translate('partials.project.statistics.last14Days').then(function (translatedValue) { + var range = {name: translatedValue, value: moment().day(-14).toDate()}; + $scope.availableDateRanges.push(range); + $scope.dateRange = range; + }); + $translate('partials.project.statistics.last7Days').then(function (translatedValue) { + $scope.availableDateRanges.push({name: translatedValue, value: moment().day(-7).toDate()}); + }); + $translate('partials.project.statistics.thisMonth').then(function (translatedValue) { + $scope.availableDateRanges.push({name: translatedValue, value: moment().startOf('month').toDate()}); + }); + $translate('partials.project.statistics.thisWeek').then(function (translatedValue) { + $scope.availableDateRanges.push({name: translatedValue, value: moment().startOf('week').toDate()}); + }); + + $scope.filterByBoard = function (board) { + if ($scope.boards[0] == board) { + Project.statistics(project.shortName, $scope.dateRange.value).then(generateDashboard); + } else { + Board.statistics(board.shortName, $scope.dateRange.value).then(generateDashboard); + } + }; + + $scope.changeDateRange = function (range) { + $scope.dateRange = range; + $scope.filterByBoard($scope.boardToFilter); + }; + + Project.findBoardsInProject(project.shortName).then(function (boards) { + boards.unshift({name: "All", archived: false}); + $scope.boards = boards; + $scope.boardToFilter = boards[0]; + $scope.filterByBoard($scope.boardToFilter); + }); + }); +})(); + diff --git a/src/main/webapp/app/controllers/project/project.js b/src/main/webapp/app/controllers/project/project.js new file mode 100644 index 000000000..b50b6a9c2 --- /dev/null +++ b/src/main/webapp/app/controllers/project/project.js @@ -0,0 +1,95 @@ +(function() { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('ProjectCtrl', function($stateParams, $scope, Project, Board, User, Notification, StompClient, + project //resolved by ui-route + ) { + + var projectName = $stateParams.projectName; + $scope.projectName = projectName; + $scope.project = project; + + $scope.boardPage = 1; + $scope.boardsPerPage = 10; + $scope.maxVisibleBoardPages = 3; + + var loadBoardsInProject = function() { + User.hasPermission('READ', projectName).then(function() { + return Project.findBoardsInProject(projectName); + }).then(function(b) { + $scope.boards = b; + $scope.totalBoards = b.length; + $scope.switchBoardPage($scope.boardPage); + }); + }; + + $scope.switchBoardPage = function(page) { + $scope.currentBoards = $scope.boards.slice((page - 1) * $scope.boardsPerPage, + ((page - 1) * $scope.boardsPerPage) + $scope.boardsPerPage); + }; + + loadBoardsInProject(); + + $scope.cardProjectPage = 1; + $scope.maxVisibleCardProjectPages = 3; + + var loadUserCardsInProject = function(page) { + User.isAuthenticated().then(function() {return User.hasPermission('SEARCH')}).then(function() { + User.cardsByProject(projectName, page).then(function(cards) { + $scope.userProjectCards = cards.cards; + $scope.totalProjectOpenCards = cards.totalCards; + $scope.projectCardsPerPage = cards.itemsPerPage; + }); + }); + }; + + loadUserCardsInProject($scope.cardProjectPage - 1); + + $scope.fetchUserCardsInProjectPage = function(page) { + loadUserCardsInProject(page - 1); + }; + + + $scope.suggestBoardShortName = function(board) { + if(board == null ||board.name == null || board.name == "") { + return; + } + Board.suggestShortName(board.name).then(function(res) { + board.shortName = res.suggestion; + }); + }; + + $scope.board = {}; + $scope.isShortNameUsed = undefined; + + $scope.$watch('board.shortName', function(newVal) { + if(newVal !== undefined && newVal !== null) { + Board.checkShortName(newVal).then(function(res) { + $scope.checkedShortName = res; + }); + } else { + $scope.checkedShortName = undefined; + } + }); + + StompClient.subscribe($scope, '/event/project/' + projectName + '/board', loadBoardsInProject); + + $scope.createBoard = function(board) { + board.shortName = board.shortName.toUpperCase(); + Project.createBoard(projectName, board).then(function() { + board.name = null; + board.description = null; + board.shortName = null; + $scope.checkedShortName = undefined; + Notification.addAutoAckNotification('success', {key: 'notification.board.creation.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('error', {key: 'notification.board.creation.error'}, false); + }); + }; + + }); + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/controllers/search.js b/src/main/webapp/app/controllers/search.js new file mode 100644 index 000000000..3f9d5f70b --- /dev/null +++ b/src/main/webapp/app/controllers/search.js @@ -0,0 +1,150 @@ +(function() { + + 'use strict'; + + var module = angular.module('lavagna.controllers'); + + module.controller('SearchCtrl', function($scope, $stateParams, $location, $http, $log, $filter, $modal, ProjectCache, Search, User, LabelCache, Card) { + + function triggerSearch() { + + var searchParams = $location.search(); + + $scope.query= encodeURIComponent(searchParams.q); + $scope.page = searchParams.page || 1; + + try { + var r = Search.parse(searchParams.q); + queryString.params.q = JSON.stringify(r); + queryString.params.page = $scope.page - 1; + $http.get('api/search/card', queryString).then(function(res) { + $scope.found = res.data.found.slice(0, res.data.countPerPage); + $scope.count = res.data.count; + $scope.currentPage = res.data.currentPage+1; + $scope.countPerPage = res.data.countPerPage; + $scope.totalPages = res.data.totalPages; + $scope.pages = []; + for(var i = 1; i<=res.data.totalPages;i++) { + $scope.pages.push(i); + } + $log.debug($scope.pages); + }); + } catch(e) { + $log.debug(e); + } + } + + var queryString = { params: {}}; + + $scope.selected = {}; + + $scope.moveToPage = function(page) { + $log.debug('move to page', page); + var loc = $location.search(); + loc.page = page; + $location.search(loc); + triggerSearch(); + }; + + + function updateSelectCount() { + var cnt = 0; + for(var k in $scope.selected) { + if($scope.selected[k] !== false) { + cnt++; + } + } + $scope.selectCount = cnt; + } + + $scope.updateSelectCount = updateSelectCount; + $scope.inProject = $stateParams.projectName !== undefined; + + + if($stateParams.projectName !== undefined) { + queryString.params.projectName = $stateParams.projectName; + + ProjectCache.project($stateParams.projectName).then(function(p) { + $scope.project = p; + }); + + LabelCache.findByProjectShortName($stateParams.projectName).then(function(res) { + + $scope.labels = res; + for(var k in res) { + if(res[k].domain === 'SYSTEM' && res[k].name === 'MILESTONE') { + $scope.milestoneLabel = res[k]; + break; + } + } + }); + } + + $scope.selectAllInPage = function() { + + var projects = {}; + + for(var i = 0;i<$scope.found.length;i++) { + if(!projects[$scope.found[i].projectShortName]) { + projects[$scope.found[i].projectShortName] = []; + } + projects[$scope.found[i].projectShortName].push($scope.found[i].id); + } + + /*the user can only select the cards where he has the MANAGE_LABEL_VALUE, which is a project level property (or global)*/ + for(var proj in projects) { + User.hasPermission('MANAGE_LABEL_VALUE', proj).then((function(idsToSetAsTrue, shortProjectName) { + return function() { + for(var i = 0;i 20; + + $scope.profile.latestActivity20 = profile.latestActivity.slice(0,20); + $scope.profile.eventsGroupedByDate = groupByDate($scope.profile.latestActivity20, $filter); + return profile; + }; + + var showCalHeatMap = function(profile, $scope) { + + + var parser = function (data) { + var stats = {}; + for (var d in data) { + stats[data[d].date / 1000] = data[d].count; + } + return stats; + }; + + var currentDate = new Date(); + var lastYear = new Date(currentDate.getFullYear(), currentDate.getMonth() - 11, 1); + + + + if($scope.cal) { + $scope.cal.update(profile.dailyActivity, parser); + } else { + $scope.cal = new CalHeatMap(); + $scope.cal.init({ + data: profile.dailyActivity, + afterLoadData: parser, + domainDynamicDimension: true, + start: lastYear, + id: "graph_c", + domain: "month", + subDomain: "day", + range: 12, + cellPadding: 2, + itemName: ["event", "events"] + }); + } + }; + + var setUserInfo = function($stateParams, $scope) { + $scope.userProvider = $stateParams.provider; + $scope.userUsername = $stateParams.username; + } + + module.controller('UserCtrl', function ($stateParams, $scope, $filter, $location, User) { + + setUserInfo($stateParams, $scope); + + $scope.userNameProfile = $stateParams.username; + + User.isCurrentUser($stateParams.provider, $stateParams.username).then(function(res) { + $scope.isCurrentUser = res; + }); + + User.getUserProfile($stateParams.provider, $stateParams.username, 0) + .then(function(profile) {return loadUser(profile, $scope, $filter);}) + .then(function(profile) {showCalHeatMap(profile, $scope)}); + }); + + + module.controller('UserActivityCtrl', function ($stateParams, $scope, $filter, $location, User) { + + setUserInfo($stateParams, $scope); + + $scope.page = 0; + + $scope.userNameProfile = $stateParams.username; + + User.isCurrentUser($stateParams.provider, $stateParams.username).then(function(res) { + $scope.isCurrentUser = res; + }); + + $scope.loadFor = function(page) { + User.getUserProfile($stateParams.provider, $stateParams.username, page) + .then(function(profile) {return loadUser(profile, $scope, $filter);}) + .then(function(profile) {showCalHeatMap(profile, $scope)}).then(function() { + $scope.page = page + }) + }; + + $scope.loadFor(0); + }) + + module.controller('UserProjectsCtrl', function ($stateParams, $scope, $filter, $location, User) { + + setUserInfo($stateParams, $scope); + + $scope.userNameProfile = $stateParams.username; + + User.isCurrentUser($stateParams.provider, $stateParams.username).then(function(res) { + $scope.isCurrentUser = res; + }); + + User.getUserProfile($stateParams.provider, $stateParams.username, 0).then(function(profile) {loadUser(profile, $scope, $filter);}) + }) + +})(); diff --git a/src/main/webapp/app/directives/lvg-board-card-menu.js b/src/main/webapp/app/directives/lvg-board-card-menu.js new file mode 100644 index 000000000..8467cae15 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-board-card-menu.js @@ -0,0 +1,130 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + //FIXME this directive is hacky + directives.directive('lvgBoardCardMenu', function ($rootScope, $compile, $filter, Card, Board) { + return { + restrict: 'A', + link: function($scope, element, attrs) { + + + $scope.openCardMenu = function(card, columns) { + + //we create a specific scope for the card menu, as the current $scope can be destroyed at any time + var $scopeForCardMenu = $rootScope.$new(); + + + var cleanup = function () { + $('#cardModal,#cardModalBackdrop').removeClass('in'); + $('#cardModal,#cardModalBackdrop').remove(); + $("#cardBoardMenu").remove(); + $(document).unbind('keyup', escapeHandler); + + $scopeForCardMenu.$destroy(); + }; + + var escapeHandler = function (e) { + if (e.keyCode == 27) { + cleanup(); + } + }; + + //FIXME this is a ugly hack: copy the necessary stuff to our new scope + var variableToCopy = ['labelNameToId', 'hasSystemLabelByName', 'hasUserLabels', + 'projectName', 'boardName', 'labelNameToId', 'addUserLabelValue', + 'removeLabelValueForId', 'isSelfWatching', 'isAssignedToCard', 'moveCard', + 'currentUserId']; + for(var k in variableToCopy) { + $scopeForCardMenu[variableToCopy[k]] = $scope[variableToCopy[k]]; + } + + // + + $scopeForCardMenu.card = card; + $scopeForCardMenu.columns = columns; + + $scopeForCardMenu.close = function() { + cleanup(); + } + + $scopeForCardMenu.move = function(card, toColumn) { + if(angular.isUndefined(toColumn)) { + return; + } + + Card.findByColumn(toColumn.id).then(function(cards) { + var ids = []; + for (var i = 0;i')); + $("#cardModal,#cardModalBackdrop").addClass('in'); + + var template = $compile('
' + + '
' + + '
' + + '
' + + '
')($scopeForCardMenu); + $("body").append(template); + + //card overflows + if(windowHeight - top < height) { + top = windowHeight - (height + 10); + } + + $("#cardBoardMenu").css({ + 'position' : 'fixed', + 'top' : top, + 'left' : left, + 'width' : size + 'px' + }); + + //calculate actions position + var actionsCloseCSSObject = {}; + var actionsCSSObject = {}; + + if(windowHeight - top < 260) { + actionsCSSObject['bottom'] = 0; + } else { + actionsCSSObject['top'] = 0; + } + + if(left + size + 30 > windowWidth - size) { + actionsCSSObject['right'] = size + 10; + actionsCloseCSSObject['right'] = 0; + actionsCSSObject['text-align'] = "right"; + } else { + actionsCSSObject['left'] = size + 10; + actionsCloseCSSObject['left'] = 0; + actionsCSSObject['text-align'] = "left"; + } + $("#cardBoardMenuActions").css(actionsCSSObject); + $("#cardBoardMenuClose").css(actionsCloseCSSObject); + + } + } + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-board-sidebar.js b/src/main/webapp/app/directives/lvg-board-sidebar.js new file mode 100644 index 000000000..345004add --- /dev/null +++ b/src/main/webapp/app/directives/lvg-board-sidebar.js @@ -0,0 +1,109 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgBoardSidebar', function (Board, Card, User, StompClient, $rootScope, $stateParams) { + var subscriptionScope = undefined; + + return { + restrict: 'A', + controller: function ($scope) { + }, + link: function ($scope, $element, $attr) { + + $scope.switchLocation = function (location) { + $("#sidebar-drop-zone").hide(); + + if (subscriptionScope !== undefined) { + subscriptionScope.$destroy(); + } + + + + subscriptionScope = $scope.$new(); + if($scope.sideBarLocation && $scope.sideBarLocation != 'NONE') { + $scope.sideBarLoad(0); + + StompClient.subscribe(subscriptionScope, '/event/board/' + $scope.boardName + '/location/' + $scope.sideBarLocation + '/card', function () { + $scope.sideBarLoad(0); + }); + } + }; + + + $scope.sideBarLoad = function (direction) { + $scope.sidebarLoaded = false; + $scope.sidebar = $scope.sidebar || {}; + if ($scope.sidebar[$scope.sideBarLocation] === undefined) { + $scope.sidebar[$scope.sideBarLocation] = {currentPage: 0}; + } + Board.cardsInLocationPaginated($scope.boardName, $scope.sideBarLocation, $scope.sidebar[$scope.sideBarLocation].currentPage + direction).then(function (res) { + if (res.length === 0) { + Board.cardsInLocationPaginated($scope.boardName, $scope.sideBarLocation, 0).then(function (res) { + $scope.sidebar[$scope.sideBarLocation] = {currentPage: 0, found: res.slice(0, 10), hasMore: res.length === 11}; + }); + } else { + $scope.sidebar[$scope.sideBarLocation] = {currentPage: $scope.sidebar[ $scope.sideBarLocation].currentPage + direction, found: res.slice(0, 10), hasMore: res.length === 11}; + } + $scope.sidebarLoaded = true; + }); + }; + + $("#sidebar-drop-zone").sortable({ + receive: function (ev, ui) { + var cardId = ui.item.attr('data-lvg-card-id'); + ui.item.data('hasUpdate', false); // disable the move card to column logic + ui.item.hide(); + if (cardId !== undefined) { + Card.moveAllFromColumnToLocation(ui.item.attr('data-lavagna-card-column-id'), [cardId], $scope.sideBarLocation); + } + } + }); + + //TODO: move this in another directive, and create a single sortable for each card :D + User.hasPermission('MOVE_CARD', $stateParams.projectName).then(function () { + $scope.sortableCardOptionsForSidebar = { + connectWith: ".lavagna-board-cards", + placeholder: "lavagna-card-placeholder", + start: function (e, ui) { + ui.placeholder.height(ui.helper.outerHeight()); + ui.item.data('initialColumnId', ui.item.attr('data-lavagna-card-column-id')); + + }, + stop: function (e, ui) { + if (ui.item.data('hasUpdate')) { + var cardId = parseInt(ui.item.attr('data-lvg-card-id'), 10); + var oldColumnId = parseInt(ui.item.data('initialColumnId'), 10); + var newColumnId = parseInt(ui.item.data('newColumnId'), 10); + var ids = ui.item.parent().sortable("toArray", {attribute: 'data-lvg-card-id'}).map(function (i) { + return parseInt(i, 10); + }); + ui.item.addClass('lavagna-to-be-cleaned-up'); + ui.item.replaceWith(ui.item.clone()); + Board.moveCardToColumn(cardId, oldColumnId, newColumnId, {newContainer: ids}); + } + ui.item.removeData('hasUpdate'); + ui.item.removeData('initialColumnId'); + ui.item.removeData('newColumnId'); + }, + update: function (e, ui) { + ui.item.data('newColumnId', ui.item.parent().parent().parent().attr('data-lvg-column-id')); + ui.item.data('hasUpdate', true); + } + }; + }, function () { + $scope.sortableCardOptionsForSidebar = false; + }); + $scope.$watch('sideBarLocation', function() { + if($scope.sideBarLocation === undefined) { + return; + } + $scope.switchLocation($scope.sideBarLocation); + }); + + } + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-bulk-action-inline.js b/src/main/webapp/app/directives/lvg-bulk-action-inline.js new file mode 100644 index 000000000..be88238f8 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-bulk-action-inline.js @@ -0,0 +1,93 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgBulkActionInline', function () { + return { + templateUrl: 'partials/fragments/bulk-action-inline.html', + restrict: 'E', + scope: { + mode: '=', + formatBulkRequest: '=', + selectedVisibleCardsIdByColumnId : '=', + afterBulkCompletion : '=' + }, + + controller: function($stateParams, $scope, Label, LabelCache, BulkOperations, Card) { + //ugly + if($stateParams.projectName !== undefined) { + LabelCache.findByProjectShortName($stateParams.projectName).then(function(res) { + for(var k in res) { + if(res[k].domain === 'SYSTEM' && res[k].name === 'MILESTONE') { + $scope.milestoneLabel = res[k]; + break; + } + } + + + $scope.userLabels = []; + for(var k in res) { + if(res[k].domain === 'USER') { + $scope.userLabels.push(res[k]); + } + } + }); + } + + + $scope.confirmMove = function(location) { + var selectedByColumnId = $scope.selectedVisibleCardsIdByColumnId(); + for(var columnId in selectedByColumnId) { + Card.moveAllFromColumnToLocation(columnId, selectedByColumnId[columnId], location); + } + }; + + var applyIfPresent = function() { + if($scope.afterBulkCompletion) { + $scope.afterBulkCompletion(); + } + }; + + // + $scope.assign = function(user) { + BulkOperations.assign($scope.formatBulkRequest(), user).then(applyIfPresent); + }; + + $scope.removeAssign = function(user) { + BulkOperations.removeAssign($scope.formatBulkRequest(), user).then(applyIfPresent); + }; + + $scope.reassign = function(user) { + BulkOperations.reassign($scope.formatBulkRequest(), user).then(applyIfPresent); + }; + + $scope.setDueDate = function(dueDate) { + BulkOperations.setDueDate($scope.formatBulkRequest(), dueDate).then(applyIfPresent); + }; + + $scope.removeDueDate = function() { + BulkOperations.removeDueDate($scope.formatBulkRequest()).then(applyIfPresent); + }; + + $scope.setMilestone = function(milestone) { + BulkOperations.setMilestone($scope.formatBulkRequest(), milestone).then(applyIfPresent); + }; + + $scope.removeMilestone = function() { + BulkOperations.removeMilestone($scope.formatBulkRequest()).then(applyIfPresent); + }; + + $scope.addLabel = function(labelToAdd) { + var labelValueToAdd = Label.extractValue(labelToAdd.label, labelToAdd.value); + BulkOperations.addLabel($scope.formatBulkRequest(), labelToAdd.label, labelValueToAdd).then(applyIfPresent); + }; + + $scope.removeLabel = function(toRemoveLabel) { + BulkOperations.removeLabel($scope.formatBulkRequest(), toRemoveLabel).then(applyIfPresent); + }; + } + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-activity.js b/src/main/webapp/app/directives/lvg-card-activity.js new file mode 100644 index 000000000..0ab98d324 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-activity.js @@ -0,0 +1,123 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardActivity', function () { + return { + templateUrl: 'partials/fragments/card-activity-entry-fragment.html', + restrict: 'E', + scope: false + } + }); + + directives.directive('lvgActivityColumn', function (BoardCache) { + return { + template: '', + restrict: 'A', + scope: { + columnId: "=lvgActivityColumn" + }, + link: function ($scope, element, attr) { + var placeholder = element.find('.lvg-activity-column-placeholder'); + BoardCache.column($scope.columnId).then(function (column) { + placeholder.text(column.columnName); + }); + } + } + }); + + directives.directive('lvgActivityLabel', function () { + return { + templateUrl: 'partials/fragments/card-activity-label-name-value-fragment.html', + restrict: 'A', + replace: true, + scope: { + activity: "=lvgActivityLabel" + } + } + }); + + directives.directive('lvgActivityActionList', function (CardCache) { + return { + template: '', + restrict: 'A', + scope: { + actionlist: '=lvgActivityActionList', + actionListId: '=', + cardId: '=' + }, + link: function ($scope, element, attr) { + $scope.value = $scope.actionlist; + if ($scope.value === undefined) { + CardCache.cardData($scope.actionListId).then(function (actionlist) { + $scope.value = actionlist; + }); + } + } + } + }); + + directives.directive('lvgActivityActionItem', function (CardCache) { + return { + template: '', + restrict: 'A', + scope: { + actionitem: '=lvgActivityActionItem', + actionItemId: '=', + cardId: '=' + }, + link: function ($scope, element, attr) { + $scope.value = $scope.actionitem; + if ($scope.value === undefined) { + CardCache.cardData($scope.actionItemId).then(function (actionitem) { + $scope.value = actionitem; + }); + } + } + } + }); + + directives.directive('lvgActivityFile', function () { + return { + template: '', + restrict: 'A', + scope: { + file: '=lvgActivityFile', + activity: '=' + }, + link: function ($scope) { + $scope.value = $scope.file; + if ($scope.value === undefined) { + $scope.value = { + name: $scope.activity.valueString + }; + } + } + } + }); + + directives.directive('lvgActivityComment', function (CardCache) { + return { + template: '', + restrict: 'A', + scope: { + activity: '=lvgActivityComment' + }, + link: function ($scope, element) { + if ($scope.activity === undefined) { + return; + } + CardCache.card($scope.activity.cardId).then(function (card) { + + var linkPlaceholder = element.find('.lvg-comment-link-placeholder'); + linkPlaceholder.attr('href', '#/' + card.projectShortName + '/' + card.boardShortName + '-' + card.sequence + '#' + $scope.activity.dataId); + + var commentPlaceholder = element.find('.lvg-comment-placeholder'); + commentPlaceholder.text('#' + $scope.activity.dataId); + }); + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-autocomplete.js b/src/main/webapp/app/directives/lvg-card-autocomplete.js new file mode 100644 index 000000000..c359e79e3 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-autocomplete.js @@ -0,0 +1,43 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardAutocomplete', function ($stateParams, $http, $parse) { + return { + restrict: 'A', + link: function (scope, elem, attrs) { + $(elem).autocomplete({ + minLength: 1, + focus: function (event, ui) { + event.preventDefault(); + }, + source: function (request, response) { + + var params = {term: request.term.trim()}; + if($stateParams.projectName) { + params.projectName = $stateParams.projectName + } + + $http.get('api/search/autocomplete-card', {params: params}).then(function (res) { + response($.map(res.data, function (card) { + return { + label: card.boardShortName + "-" + card.sequence + " " + card.name, + value: card + }; + })); + }); + }, + select: function (event, ui) { + event.preventDefault(); + $(elem).val(ui.item.label); + scope.$apply(function () { + $parse(attrs['lvgCardAutocomplete']).assign(scope, ui.item.value); + }); + } + }); + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-board-home-panel.js b/src/main/webapp/app/directives/lvg-card-board-home-panel.js new file mode 100644 index 000000000..bd23bf57f --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-board-home-panel.js @@ -0,0 +1,40 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardBoardHomePanel', function () { + return { + templateUrl: 'partials/fragments/card-board-home-fragment.html', + restrict: 'E', + scope: { + card: '=' + }, + controller: function ($scope) { + $scope.hasUserLabels = function (cardLabels) { + if (cardLabels === undefined || cardLabels.length === 0) { + return false; //empty, no labels at all + } + for (var i = 0; i < cardLabels.length; i++) { + if (cardLabels[i].labelDomain == 'USER') { + return true; + } + } + return false; + }; + + $scope.hasSystemLabelByName = function (labelName, cardLabels) { + if (cardLabels === undefined || cardLabels.length === 0) + return false; //empty, no labels at all + for (var i = 0; i < cardLabels.length; i++) { + if (cardLabels[i].labelName == labelName && cardLabels[i].labelDomain == 'SYSTEM') { + return true; + } + } + return false; + }; + } + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-fragment-ro.js b/src/main/webapp/app/directives/lvg-card-fragment-ro.js new file mode 100644 index 000000000..93de43d47 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-fragment-ro.js @@ -0,0 +1,13 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardFragmentRo', function () { + return { + templateUrl: 'partials/fragments/board-card-menu-card-fragment.html', + restrict: 'E' + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-fragment.js b/src/main/webapp/app/directives/lvg-card-fragment.js new file mode 100644 index 000000000..46ee22237 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-fragment.js @@ -0,0 +1,13 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardFragment', function () { + return { + templateUrl: 'partials/fragments/card-fragment.html', + restrict: 'E' + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-modal.js b/src/main/webapp/app/directives/lvg-card-modal.js new file mode 100644 index 000000000..cdd05d365 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-modal.js @@ -0,0 +1,55 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardModal', function () { + return { + restrict: 'A', + transclude: true, + controller: function ($scope, $element, $state) { + + $scope.goBack = function() { + //go to parent state + $state.go('^'); + } + }, + template: '
', + link: function ($scope) { + var cleanup = function () { + $('#cardModal,#cardModalBackdrop').removeClass('in'); + $('#cardModal,#cardModalBackdrop').remove(); + $("body").removeClass('lvg-modal-open'); + $(document).unbind('keyup', $scope.escapeHandler); + }; + + $scope.close = function () { + cleanup(); + $scope.goBack(); + }; + + $scope.$on('$destroy', cleanup); + + $scope.escapeHandler = function (e) { + if (e.keyCode == 27) { + $scope.$apply($scope.close); + } + }; + + $(document).bind('keyup', $scope.escapeHandler); + + $scope.clickHandler = function (event) { + if (event.target.id == 'cardModal') { + $scope.close(); + } + }; + + $("body").append($('
')); + $("body").addClass('lvg-modal-open'); + $("#cardModal,#cardModalBackdrop").addClass('in'); + } + }; + }); + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-ro.js b/src/main/webapp/app/directives/lvg-card-ro.js new file mode 100644 index 000000000..a84cc72b4 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-ro.js @@ -0,0 +1,50 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardRo', function ($rootScope, CardCache) { + + var loadCard = function (cardId, shortNamePlaceholder, namePlaceholder, noName) { + CardCache.card(cardId).then(function (card) { + shortNamePlaceholder.text(card.boardShortName + '-' + card.sequence); + if (card.columnDefinition != 'CLOSED') { + shortNamePlaceholder.removeClass('lavagna-closed-card'); + } else { + shortNamePlaceholder.addClass('lavagna-closed-card'); + } + if (!noName) { + namePlaceholder.text(card.name); + } + }); + }; + + return { + restrict: 'A', + transclude: true, + scope: true, + template: '' + + ' ', + link: function ($scope, element, attrs) { + var unregister = $scope.$watch(attrs.lvgCardRo, function (cardId) { + if (cardId == undefined) { + return; + } + var namePlaceholder = element.find('.lavagna-card-name-placeholder'); + var shortNamePlaceholder = element.find('.lavagna-card-short-placeholder'); + + var noName = 'noName' in attrs; + loadCard(cardId, shortNamePlaceholder, namePlaceholder, noName); + + var unbind = $rootScope.$on('refreshCardCache-' + cardId, function () { + loadCard(cardId, shortNamePlaceholder, namePlaceholder, noName); + }); + $scope.$on('$destroy', unbind); + + unregister(); + }); + } + }; + }); +})(); diff --git a/src/main/webapp/app/directives/lvg-card-search-result.js b/src/main/webapp/app/directives/lvg-card-search-result.js new file mode 100644 index 000000000..6cce5dc9c --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-search-result.js @@ -0,0 +1,37 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardSearchResult', function () { + return { + templateUrl: 'partials/fragments/card-search-result-fragment.html', + restrict: 'E', + controller: function ($scope) { + $scope.hasUserLabels = function (cardLabels) { + if (cardLabels === undefined || cardLabels.length === 0) { + return false; //empty, no labels at all + } + for (var i = 0; i < cardLabels.length; i++) { + if (cardLabels[i].labelDomain == 'USER') { + return true; + } + } + return false; + }; + + $scope.hasSystemLabelByName = function (labelName, cardLabels) { + if (cardLabels === undefined || cardLabels.length === 0) + return false; //empty, no labels at all + for (var i = 0; i < cardLabels.length; i++) { + if (cardLabels[i].labelName == labelName && cardLabels[i].labelDomain == 'SYSTEM') { + return true; + } + } + return false; + }; + } + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card-sidebar-fragment.js b/src/main/webapp/app/directives/lvg-card-sidebar-fragment.js new file mode 100644 index 000000000..6720666eb --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card-sidebar-fragment.js @@ -0,0 +1,13 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCardSidebarFragment', function () { + return { + templateUrl: 'partials/fragments/card-sidebar-fragment.html', + restrict: 'E' + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-card.js b/src/main/webapp/app/directives/lvg-card.js new file mode 100644 index 000000000..f3e09e155 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-card.js @@ -0,0 +1,52 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCard', function ($rootScope, CardCache) { + + var loadCard = function (cardId, linkPlaceholder, shortNamePlaceholder, namePlaceholder, noName) { + CardCache.card(cardId).then(function (card) { + shortNamePlaceholder.text(card.boardShortName + '-' + card.sequence); + if (card.columnDefinition != 'CLOSED') { + shortNamePlaceholder.removeClass('lavagna-closed-card'); + } else { + shortNamePlaceholder.addClass('lavagna-closed-card'); + } + if (!noName) { + namePlaceholder.text(card.name); + } + linkPlaceholder.attr('href', '#/' + card.projectShortName + '/' + card.boardShortName + '-' + card.sequence); + }); + }; + + return { + restrict: 'A', + transclude: true, + scope: true, + template: '' + + ' ', + link: function ($scope, element, attrs) { + var unregister = $scope.$watch(attrs.lvgCard, function (cardId) { + if (cardId == undefined) { + return; + } + var linkPlaceholder = element.find('.lavagna-card-link-placeholder'); + var namePlaceholder = element.find('.lavagna-card-name-placeholder'); + var shortNamePlaceholder = element.find('.lavagna-card-short-placeholder'); + + var noName = 'noName' in attrs; + loadCard(cardId, linkPlaceholder, shortNamePlaceholder, namePlaceholder, noName); + + var unbind = $rootScope.$on('refreshCardCache-' + cardId, function () { + loadCard(cardId, linkPlaceholder, shortNamePlaceholder, namePlaceholder, noName); + }); + $scope.$on('$destroy', unbind); + + unregister(); + }); + } + }; + }); +})(); diff --git a/src/main/webapp/app/directives/lvg-chart.js b/src/main/webapp/app/directives/lvg-chart.js new file mode 100644 index 000000000..f77e0d642 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-chart.js @@ -0,0 +1,41 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgChart', function () { + var baseWidth = 150; + var baseHeight = 150; + + return { + restrict: 'E', + template: '', + scope: { + data: "=data", + options: "=options", + type: "@", + width: "@", + height: "@" + }, + link: function (scope, element, attrs) { + scope.$watch('data', function (value) { + if (value === undefined) { + return; + } + + var canvas = element.find('canvas')[0]; + var context = canvas.getContext('2d'); + + canvas.width = scope.width || baseWidth; + canvas.height = scope.height || baseHeight; + context.canvas.style.maxHeight = canvas.height + "px"; + + var chart = new Chart(context); + var chartType = scope.type || "Line"; + chart[chartType](scope.data, scope.options); + }); + } + } + }); +})(); diff --git a/src/main/webapp/app/directives/lvg-codemirror.js b/src/main/webapp/app/directives/lvg-codemirror.js new file mode 100644 index 000000000..bd2acbb04 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-codemirror.js @@ -0,0 +1,58 @@ +(function () { + + //some parts were taken from https://github.com/angular-ui/ui-codemirror/blob/master/src/ui-codemirror.js + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgCodemirror', function ($timeout, $translate) { + return { + restrict: 'A', + require: '?ngModel', + link: function (scope, element, attrs, ngModel) { + + function prepare() { + var editor = CodeMirror.fromTextArea(element[0], { + /*lineNumbers: true, cause some ugly artifact on first load */ + lineWrapping: true, + mode: "markdown", + viewportMargin: Infinity + }); + + editor.on("change", function () { + ngModel.$setViewValue(editor.getValue()); + if (!scope.$$phase) { + scope.$apply(); + } + }); + + editor.on('keyup', function (cm, event) { + //esc + if (event.keyCode == 27) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + ngModel.$render = function () { + editor.setValue(ngModel.$viewValue || ''); + //TODO: check, without timeout, the editor keep the initial empty size + $timeout(function () { + editor.refresh(); + }); + }; + } + + if (attrs.codemirrorPlaceholder) { + $translate(attrs.codemirrorPlaceholder).then(function (text) { + $(element).attr('placeholder', text); + prepare(); + }); + } else { + prepare(); + } + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-colorpicker.js b/src/main/webapp/app/directives/lvg-colorpicker.js new file mode 100644 index 000000000..384476fb7 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-colorpicker.js @@ -0,0 +1,75 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgColorpicker', function () { + return { + restrict: 'E', + require: 'ngModel', + scope: false, + template: '', + link: function ($scope, $element, attrs, $ngModel) { + + var $input = $element.find('input'); + var fallbackValue = $scope.$eval(attrs.fallbackValue); + + function setViewValue(color) { + var value = fallbackValue; + + if (color) { + value = color.toString(); + } else if (angular.isUndefined(fallbackValue)) { + value = color; + } + + $ngModel.$setViewValue(value); + } + + var onChange = function (color) { + $scope.$apply(function () { + setViewValue(color.toHexString()); + }); + }; + var onToggle = function () { + $input.spectrum('toggle'); + return false; + }; + var options = angular.extend({ + color: $ngModel.$viewValue, + change: onChange, + move: onChange, + hide: onChange, + showPalette: true, + palette: [ + ['#e51c23', '#e91e63', '#9c27b0', '#673ab7'], + ['#3f51b5', '#5677fc', '#03a9f4', '#00bcd4'], + ['#009688', '#259b24', '#8bc34a', '#cddc39'], + ['#ffeb3b', '#ffc107', '#ff9800', '#ff5722'] + ] + }, $scope.$eval(attrs.options)); + + + if (attrs.triggerId) { + angular.element(document.body).on('click', '#' + attrs.triggerId, onToggle); + } + + $ngModel.$render = function () { + $input.spectrum('set', $ngModel.$viewValue || ''); + }; + + if (options.color) { + $input.spectrum('set', options.color || ''); + setViewValue(options.color.toHexString()); + } + + $input.spectrum(options); + + $scope.$on('$destroy', function () { + $input.spectrum('destroy'); + }); + } + }; + }); +})(); diff --git a/src/main/webapp/app/directives/lvg-datepicker.js b/src/main/webapp/app/directives/lvg-datepicker.js new file mode 100644 index 000000000..b80e06dd2 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-datepicker.js @@ -0,0 +1,23 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgDatepicker', function () { + return { + restrict: 'A', + require: '?ngModel', + link: function (scope, element, attrs, ngModel) { + element.datepicker({ + dateFormat: 'dd.mm.yy', + onSelect: function (date) { + scope.$apply(function () { + ngModel.$setViewValue(date); + }); + } + }); + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-download.js b/src/main/webapp/app/directives/lvg-download.js new file mode 100644 index 000000000..a98ed779a --- /dev/null +++ b/src/main/webapp/app/directives/lvg-download.js @@ -0,0 +1,16 @@ +(function() { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgDownload', function(CONTEXT_PATH) { + return function($scope, element, attrs) { + $(element).click(function() { + var url = CONTEXT_PATH+$(this).attr('href'); + $(element).after(""); + return false; + }); + } + }) +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-fill-height.js b/src/main/webapp/app/directives/lvg-fill-height.js new file mode 100644 index 000000000..cd423f88e --- /dev/null +++ b/src/main/webapp/app/directives/lvg-fill-height.js @@ -0,0 +1,196 @@ +(function() { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + /* + * we have the following situation: + * + * +----------------------+ + + * | top | | + * +--------------------- +-------------------------- $(element).position().top + * | +--- window.height() + * | canvas area | + * | | + * | | + * +---------------------- + + * + * + * The canvas area must have an height that fill the current view port if we + * want that the horizontal scroll bar to appear at the bottom of the page. + * + * Additionally, we support the presence of a left sidebar with an absolute + * position. + * + * + * Thus: + */ + directives.directive('lvgFillHeight', function($window, $timeout) { + return { + restrict : 'A', + link: function($scope, element, attrs) { + var $elem = $(element); + + var resizeHandler = function() { + var toggle = "true" === attrs.lvgFillHeight; + var margin = parseInt(attrs.lvgFillHeightMargin, 10) || 250; + //sidebar related + //-> the sidebar has a width of 250px + var wHeight = $window.innerHeight;// + var wWidth = $window.innerWidth;// + $elem.width(wWidth - (toggle ? margin : 0)); + + var maxHeight = wHeight - $elem.position().top; + + $elem.height(wHeight - $elem.position().top); + + $timeout(function() {$scope.maxHeight = maxHeight;},0); + + + }; + resizeHandler(); + + //sidebar related + attrs.$observe('lvgFillHeight', resizeHandler); + + $($window).resize(resizeHandler); + + $scope.$watch(function() { + return $(element).position().top; + }, function() { + resizeHandler(); + } + ); + + $scope.$on('$destroy', function() { + $($window).unbind('resize', resizeHandler) + }); + } + }; + }); + + directives.directive('lvgUpdateColumnSize', function($window, $timeout) { + return { + restrict : 'A', + link : function($scope, element, attrs) { + + var panelHead = element.find('.panel-heading')[0]; + var panelFooter = element.find('.panel-footer')[0]; + + $scope.$watch(function() {return panelHead.offsetHeight + panelFooter.offsetHeight;}, function(v) { + $scope.panelHeadAndFooterSize = v; + }); + } + }; + }); + + directives.directive('lvgFillSidebarHeight', function($window) { + return { + restrict : 'A', + link: function($scope, element, attrs) { + var $elem = $(element); + + var resizeHandler = function() { + var wHeight = $window.innerHeight;// + $elem.height(wHeight - 102); //fixed + }; + resizeHandler(); + + $($window).resize(resizeHandler); + + $scope.$on('$destroy', function() { + $($window).unbind('resize', resizeHandler) + }); + + //sidebar related + attrs.$observe('lvgFillSidebarHeight', resizeHandler); + } + }; + }); + + directives.directive('lvgFillSidebarCardsHeight', function($window) { + return { + restrict : 'A', + link: function($scope, element, attrs) { + var $elem = $(element); + + var resizeHandler = function() { + var parentHeight = $window.innerHeight - 120;//fixed + var offset = $elem.get(0).offsetTop; + + var hasMoreCards = attrs.lvgFillSidebarCardsHeightHasMore === "true"; + var currentPage = parseInt(attrs.lvgFillSidebarCardsHeightCurrentPage, 0) || 0; + + if(hasMoreCards || currentPage) { + offset = offset + 36; + } + + $elem.height(parentHeight - offset - 14); //14: based on margin, padding + }; + resizeHandler(); + $($window).resize(resizeHandler); + + $scope.$on('$destroy', function() { + $($window).unbind('resize', resizeHandler) + }); + + //sidebar related + attrs.$observe('lvgFillSidebarCardsHeight', resizeHandler); + attrs.$observe('lvgFillSidebarCardsHeightHasMore', resizeHandler); + attrs.$observe('lvgFillSidebarCardsHeightCurrentPage', resizeHandler); + } + }; + }); + + //hack + directives.directive('lvgContainerMaxWidth', function($window) { + return { + restrict : 'A', + link: function($scope, element, attrs) { + var getBootstrapWidth = function(width, toggle, margin) { + var subtractWidth = 0; + if(toggle) { + subtractWidth += margin + 60; + } + + var trueWidth = width - subtractWidth; + + if(trueWidth >= 1200) { + return 1140; + } + + if(trueWidth >= 992) { + return 940; + } + + if(trueWidth >= 768) { + return 720; + } + + return trueWidth - 30; + + } + + var resizeHandler = function() { + var toggle = parseInt(attrs.lvgContainerMaxWidth, 10); + var margin = parseInt(attrs.lvgContainerMaxWidth, 10) || 250; + + // + var wWidth = $window.innerWidth;// + $(element).css('max-width', getBootstrapWidth(wWidth, toggle + 'px', margin)); + }; + + resizeHandler(); + + //sidebar related + attrs.$observe('lvgContainerMaxWidth', resizeHandler); + $($window).resize(resizeHandler); + + $scope.$on('$destroy', function() { + $($window).unbind('resize', resizeHandler) + }); + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-has-permission.js b/src/main/webapp/app/directives/lvg-has-permission.js new file mode 100644 index 000000000..3964b484c --- /dev/null +++ b/src/main/webapp/app/directives/lvg-has-permission.js @@ -0,0 +1,70 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgHasPermission', function (User, $stateParams) { + return { + restrict: 'A', + link: function ($scope, element, attrs) { + element.addClass('lavagna-hide'); + if (attrs.ownerId !== undefined) { + User.currentCachedUser().then(function (u) { + if (u.id === $scope.$eval(attrs.ownerId)) { + element.removeClass('lavagna-hide'); + } + }); + } + + if ('withProject' in attrs) { + User.hasPermission(attrs.lvgHasPermission, attrs.withProject).then(function () { + element.removeClass('lavagna-hide'); + }); + } else { + User.hasPermission(attrs.lvgHasPermission, $stateParams.projectName).then(function () { + element.removeClass('lavagna-hide'); + }); + } + } + }; + }); + + directives.directive('lvgHasNotPermission', function (User, $stateParams) { + return { + restrict: 'A', + link: function ($scope, element, attrs) { + element.addClass('lavagna-hide'); + User.hasPermission(attrs.lvgHasNotPermission, $stateParams.projectName).then(function () { + }, function () { + element.removeClass('lavagna-hide'); + }); + } + }; + }); + + directives.directive('lvgHasAllPermissions', function (User, $stateParams) { + return { + restrict: 'A', + link: function ($scope, element, attrs) { + element.addClass('lavagna-hide'); + User.hasAllPermissions(attrs.lvgHasAllPermissions.split(','), $stateParams.projectName).then(function () { + element.removeClass('lavagna-hide'); + }); + } + }; + }); + + + directives.directive('lvgHasAtLeastOnePermission', function (User, $stateParams) { + return { + restrict: 'A', + link: function ($scope, element, attrs) { + element.addClass('lavagna-hide'); + User.hasAtLeastOnePermission(attrs.lvgHasAtLeastOnePermission.split(','), $stateParams.projectName).then(function () { + element.removeClass('lavagna-hide'); + }); + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-is-authenticated.js b/src/main/webapp/app/directives/lvg-is-authenticated.js new file mode 100644 index 000000000..22d0bcc95 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-is-authenticated.js @@ -0,0 +1,30 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgIsAuthenticated', function (User) { + return { + restrict: 'A', + link: function ($scope, element) { + element.addClass('ng-hide'); + User.isAuthenticated().then(function () { + element.removeClass('ng-hide'); + }); + } + }; + }); + + directives.directive('lvgIsNotAuthenticated', function (User) { + return { + restrict: 'A', + link: function ($scope, element) { + element.addClass('ng-hide'); + User.isNotAuthenticated().then(function () { + element.removeClass('ng-hide'); + }); + } + }; + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-label-picker.js b/src/main/webapp/app/directives/lvg-label-picker.js new file mode 100644 index 000000000..051029ddc --- /dev/null +++ b/src/main/webapp/app/directives/lvg-label-picker.js @@ -0,0 +1,46 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgLabelPicker', function (LabelCache) { + return { + templateUrl: 'partials/fragments/label-pickers.html', + require: 'ngModel', + scope: { + model: '=ngModel', + label: '=', + board: '=', + inMenu: '=' + }, + restrict: 'E', + link: function (scope, element, attrs) { + scope.internal = {}; + + //hacky (?) + scope.$watch('internal.model', function () { + scope.model = scope.internal.model + }) + + scope.$watch('model', function() { + scope.internal.model = scope.model; + }); + + + scope.$watch('label', function () { + scope.internal.model = null; + scope.listValues = null; + if (scope.label && scope.label.type === 'LIST') { + LabelCache.findLabelListValues(scope.label.id).then(function (res) { + scope.listValues = res; + }); + } + }); + } + }; + }); +})(); + + + diff --git a/src/main/webapp/app/directives/lvg-label-val.js b/src/main/webapp/app/directives/lvg-label-val.js new file mode 100644 index 000000000..afd43207f --- /dev/null +++ b/src/main/webapp/app/directives/lvg-label-val.js @@ -0,0 +1,55 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgLabelVal', function ($filter, Card, LabelCache) { + + var loadListValue = function (labelId, listValueId, scope) { + LabelCache.findLabelListValue(labelId, listValueId).then(function (listValue) { + scope.displayValue = listValue.value; + }); + }; + + return { + restrict: 'EA', + scope: { + value: '=' + }, + template: '' + + '' + + '' + + '' + + '' + + '', + link: function ($scope, $element, $attrs) { + + if ($scope.value === undefined || $scope.value === null) { + return; + } + + $scope.readOnly = $attrs.readOnly != undefined; + + $scope.type = $scope.value.labelValueType || $scope.value.type || $scope.value.labelType; + + var type = $scope.type; + var value = $scope.value.value || $scope.value; + if (type === 'STRING') { + $scope.displayValue = value.valueString; + } else if (type === 'INT') { + $scope.displayValue = value.valueInt; + } else if (type === 'USER') { + $scope.displayValue = value.valueUser; + } else if (type === 'CARD') { + $scope.displayValue = value.valueCard; + } else if (type === 'LIST') { + loadListValue($scope.value.labelId, value.valueList, $scope); + } else if (type === 'TIMESTAMP') { + $scope.displayValue = $filter('date')(value.valueTimestamp, 'dd.MM.yyyy'); + } + } + }; + }); + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-label.js b/src/main/webapp/app/directives/lvg-label.js new file mode 100644 index 000000000..b97a15003 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-label.js @@ -0,0 +1,31 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgLabel', function () { + return { + transclude: true, + restrict: 'AE', + scope: { + value: '=', + name: '=' + }, + template: '' + + '' + + ': ' + + ' ' + + ' ' + + '' + + '', + link: function ($scope, $element, $attrs) { + if ($scope.value === null || $scope.value === undefined || $scope.name === null || $scope.name === undefined) { + return; + } + $scope.readOnly = $attrs.readOnly != undefined; + $scope.type = $scope.value.labelValueType || $scope.value.type; + } + }; + }) +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-load-script.js b/src/main/webapp/app/directives/lvg-load-script.js new file mode 100644 index 000000000..91ebc075c --- /dev/null +++ b/src/main/webapp/app/directives/lvg-load-script.js @@ -0,0 +1,28 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + /** ngSrc inside a script in a partial don't seems to work. */ + directives.directive('lvgLoadScript', function () { + return { + restrict: 'A', + scope: { + lvgLoadScript: "@" + }, + link: function ($scope, element, attrs) { + element.addClass('ng-hide'); + $.getScript($scope.lvgLoadScript).done(function (script, textStatus) { + // here in theory we could enable the button + }) + .fail(function (jqxhr, settings, exception) { + // here in theory we could disable the button + error + // message + element.removeClass('ng-hide'); + }); + } + } + }); + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-manage-label.js b/src/main/webapp/app/directives/lvg-manage-label.js new file mode 100644 index 000000000..8a31ef27c --- /dev/null +++ b/src/main/webapp/app/directives/lvg-manage-label.js @@ -0,0 +1,101 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgManageLabel', function ($rootScope, $modal, Label, $stateParams, LabelCache, Notification, $filter) { + return { + restrict: 'A', + scope: { + label: '=lvgManageLabel', + }, + templateUrl: "partials/project/fragments/project-manage-label.html", + controller: function ($scope) { + var projectName = $stateParams.projectName; + + var emitRefreshEvent = function() { + $scope.$emit('refreshLabelCache-' + projectName); + } + + $scope.removeLabel = function () { + Label.remove($scope.label.id).then(function() { + Notification.addAutoAckNotification('success', {key: 'notification.project-manage-labels.remove.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('success', {key: 'notification.project-manage-labels.remove.error'}, false); + }).then(emitRefreshEvent); + }; + + $scope.updateLabel = function (values) { + var labelColor = $filter('parseHexColor')(values.color); + Label.update($scope.label.id, {name: values.name, color: labelColor, type: values.type}).then(function() { + Notification.addAutoAckNotification('success', {key: 'notification.project-manage-labels.update.success'}, false); + }, function(error) { + Notification.addAutoAckNotification('success', {key: 'notification.project-manage-labels.update.error'}, false); + }).then(emitRefreshEvent); + }; + + $scope.labelsListValues = {}; + var loadListValues = function () { + if ($scope.label.type === 'LIST') { + LabelCache.findLabelListValues($scope.label.id).then(function (listValues) { + $scope.labelsListValues = listValues; + }); + } + }; + + var isLabelInUse = function() { + Label.useCount($scope.label.id).then(function(useCount) { + $scope.useCount = useCount; + }); + } + + var loadLabelData = function() { + loadListValues(); + isLabelInUse(); + } + + loadLabelData(); + + var unbind = $rootScope.$on('refreshLabelCache-' + projectName, loadLabelData); + $scope.$on('$destroy', unbind); + + $scope.editLabelList = function (label, labelsListValues) { + $modal.open({ + templateUrl: 'partials/project/fragments/project-modal-edit-label.html', + windowClass: 'lavagna-modal', + controller: function ($rootScope, $scope, LabelCache, Label) { + $scope.l = label; + $scope.labelsListValues = labelsListValues; + + $scope.swapLabelListValues = function (first, second) { + Label.swapLabelListValues($scope.l.id, {first: first, second: second}); + }; + + $scope.addLabelListValue = function (val) { + Label.addLabelListValue($scope.l.id, {value: val}); + }; + + $scope.removeLabelListValue = function (labelListValueId) { + Label.removeLabelListValue(labelListValueId); + }; + + var loadListValues = function () { + if ($scope.l.type === 'LIST') { + LabelCache.findLabelListValues($scope.l.id).then(function (listValues) { + $scope.labelsListValues = listValues; + }); + } + }; + + var unbind = $rootScope.$on('refreshLabelCache-' + projectName, loadListValues); + $scope.$on('$destroy', unbind); + }, + size: 'lg' + }); + }; + + } + }; + }) +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-navbar.js b/src/main/webapp/app/directives/lvg-navbar.js new file mode 100644 index 000000000..30b23d462 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-navbar.js @@ -0,0 +1,29 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgNavbar', ['$rootScope', '$state', function ($rootScope, $state) { + return { + restrict: 'E', + templateUrl: 'partials/fragments/lvg-navbar.html', + transclude: true, + link : function($scope, $element, $attrs) { + $scope.navigationState = $state.current.name; + + $scope.navbarType = $attrs.navbarType; + + $rootScope.$on('$stateChangeSuccess', + function (event, toState, toParams, fromState, fromParams) { + $scope.navigationState = $state.current.name; + } + ); + + $scope.stateBelongsTo = function(parentState) { + return $scope.navigationState.indexOf(parentState) == 0; + } + } + } + }]); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-navigation.js b/src/main/webapp/app/directives/lvg-navigation.js new file mode 100644 index 000000000..47df913cd --- /dev/null +++ b/src/main/webapp/app/directives/lvg-navigation.js @@ -0,0 +1,40 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgProjectNavigation', function () { + return { + restrict: 'E', + template: 'partials.home.title' + + '{{project.shortName}}-{{project.name}}' + + } + + }); + + directives.directive('lvgBoardNavigation', function () { + return { + restrict: 'E', + template: 'partials.home.title' + + '{{project.shortName}} ' + + '{{board.shortName}}-{{board.name}}' + + } + + }); + + directives.directive('lvgProjectSettingsNavigation', function () { + return { + restrict: 'E', + template: 'partials.home.title' + + '{{project.shortName}}' + + 'partials.project.fragments.nabvar.admin' + + } + + }); + + +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-notification.js b/src/main/webapp/app/directives/lvg-notification.js new file mode 100644 index 000000000..54f2dba1c --- /dev/null +++ b/src/main/webapp/app/directives/lvg-notification.js @@ -0,0 +1,12 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgNotification', function (Notification) { + return { + templateUrl: 'partials/fragments/notifications-fragment.html' + } + }) +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-on-esc.js b/src/main/webapp/app/directives/lvg-on-esc.js new file mode 100644 index 000000000..508a49ddc --- /dev/null +++ b/src/main/webapp/app/directives/lvg-on-esc.js @@ -0,0 +1,33 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgOnEsc', function () { + return { + restrict: 'A', + scope: { + onEsc: '=', + onEscEval: '@' + }, + link: function ($scope, element) { + $(element).keyup(function (e) { + if (e.keyCode == 27) { + if ($scope.onEsc || $scope.onEscEval) { + $scope.$apply(function () { + if ($scope.onEsc) { + $scope.onEsc = false; + } + if ($scope.onEscEval) { + $scope.$parent.$eval($scope.onEscEval); + } + }); + } + return false; + } + }); + } + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-scroll-to-comment.js b/src/main/webapp/app/directives/lvg-scroll-to-comment.js new file mode 100644 index 000000000..2180d73fd --- /dev/null +++ b/src/main/webapp/app/directives/lvg-scroll-to-comment.js @@ -0,0 +1,21 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgScrollToComment', function ($window, $timeout) { + return { + scope: { + commentId: "=lvgScrollToComment" + }, + link: function (scope, element) { + if ($window.location.hash === ('#' + scope.commentId)) { + $timeout(function () { + $(element).addClass('lavagna-highlight').get(0).scrollIntoView(); + }); + } + } + }; + }) +})(); diff --git a/src/main/webapp/app/directives/lvg-search-controls.js b/src/main/webapp/app/directives/lvg-search-controls.js new file mode 100644 index 000000000..3eca4b761 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-search-controls.js @@ -0,0 +1,13 @@ +(function () { + + 'use strict'; + + var directives = angular.module('lavagna.directives'); + + directives.directive('lvgSearchControls', function () { + return { + restrict: 'E', + templateUrl: 'partials/fragments/lvg-search-controls.html' + } + }); +})(); \ No newline at end of file diff --git a/src/main/webapp/app/directives/lvg-search.js b/src/main/webapp/app/directives/lvg-search.js new file mode 100644 index 000000000..27b122305 --- /dev/null +++ b/src/main/webapp/app/directives/lvg-search.js @@ -0,0 +1,331 @@ +(function () { + + 'use strict'; + + var rootSearchFilter; + var locationSearch = {}; + + function tryParse(q, Search, $log) { + try { + return Search.parse(q); + } catch (e) { + $log.debug("parsing failure", e); + return false; + } + } + + function queryIsNotEmpty(res) { + return res && res.length >= 1 && res[0] && res[0].type !== 'WHITE_SPACE'; + } + + function parseAndBroadcastForBoardSearch(query, $scope, $log, $location, $rootScope, $state, Search) { + + var r = tryParse(query, Search, $log); + + var locSearch = {}; + var searchFilterRes = undefined; + if (queryIsNotEmpty(r)) { + locSearch = {q: query}; + searchFilterRes = r; + } + + rootSearchFilter = searchFilterRes; + locationSearch = locSearch; + + if (r !== false || (r === false && query === "")) { + $rootScope.$broadcast('refreshSearch', {searchFilter: searchFilterRes, location: locSearch}); + } + + } + + function updateSearchContext(toState, fromState, $scope) { + if (toState.name.indexOf("projectBoard") === 0) { + $scope.searchContext = {name: 'Board'}; + } else if (toState.url.indexOf('/:projectName/') === 0 || toState.name.indexOf("ProjectManage.") === 0) { + $scope.searchContext = {name: 'Project'}; + } else { + $scope.searchContext = {name: 'All'}; + } + } + + function fromTagsToQuery(tags) { + + if(!angular.isArray(tags)) { + return ''; + } + + var res = ''; + + angular.forEach(tags, function(v) { + res += v.value + ' '; + }); + + return res.trim(); + } + + function fromQueryToTags(query) { + + var res = []; + + angular.forEach(query, function(v) { + var r = {}; + if(v.type !== 'FREETEXT') { + r = {value: (v.type === 'USER_LABEL' ? '#' : '') + v.name + (v.value ? (':' + quoteIfHasSpace(v.value.originalValue || v.value.value)) : '')}; + } else { + r = {value: v.value.value}; + } + res.push(r); + }); + + return res; + } + + + function quoteIfHasSpace(str) { + if(str.indexOf(' ') > -1) { + return "'"+str+"'"; + } else { + return str; + } + } + + var directives = angular.module('lavagna.directives'); + directives.directive('lvgSearch', function ($log, $location, $rootScope, $state, $stateParams, $timeout, $http, $window, $translate, $q, Search) { + + + function toParams(input, prefix) { + var q = input.substr(prefix.length).trim(); + var params = {term: q}; + if($stateParams.projectName) { + params.projectName = $stateParams.projectName + } + return params; + } + + return { + restrict: 'E', + templateUrl: 'partials/fragments/lavagna-search.html', + scope: {}, + link: function ($scope, element, attributes) { + + function searchUser(input, prefix, data) { + return $http.get('api/search/user', {params: toParams(input, prefix)}).then(function (res) { + for (var i = 0; i < res.data.length; i++) { + var u = res.data[i]; + data.push({value: prefix + u.provider + ":" + u.username + " ", text: prefix + u.provider + ":" + u.username, type: "create", user: u, prefix: prefix}); + } + return data + }); + } + + function searchMilestone(input, prefix, data) { + return $http.get('api/search/milestone', {params: toParams(input, prefix)}).then(function (res) { + for (var i = 0; i < res.data.length; i++) { + var u = res.data[i]; + data.push({value: prefix + quoteIfHasSpace(u), text: prefix + quoteIfHasSpace(u), type: "create"}); + } + return data; + }); + } + + function searchLabel(input, prefix, data) { + return $http.get('api/search/label-name', {params: toParams(input, prefix)}).then(function (res) { + for (var i = 0; i < res.data.length; i++) { + var u = res.data[i]; + data.push({value: prefix + quoteIfHasSpace(u), text: prefix + quoteIfHasSpace(u), type: "create"}); + } + return data; + }); + } + + $scope.toSearch = {}; + + $scope.configuration = { + tagTemplate:'{{tag.value}} ', + restoreOnBackspace : true, + textInputRepresentation:function(elem) { + return elem.value; + }, + autocompleteSelection : function(elem, $scope) { + if(elem && elem.type === 'example') { + $scope.ngModel.userInput = elem.value; + } else { + $scope.addTagInNaturalPosition(elem); + $scope.ngModel.userInput = ''; + } + }, + autocompleteProvider:function(input) { + + var inputIsEmpty = input === null || input === undefined || input.trim() === ''; + + // + if(inputIsEmpty) { + return false; + } + // + + var input = input.trim(); + + var examples = [{value: "#", text: "#