diff --git a/.gitattributes b/.gitattributes index 13edf646ed..72cdd367e3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,7 +5,7 @@ # converted to the OS's native line endings. * text -# Explicitly declare text files we want to always be normalized and converted +# Explicitly declare text files we want to always be normalized and converted # to native line endings on checkout. *.c text *.css text @@ -29,6 +29,10 @@ # Declare files that will always have CRLF line endings on checkout. *.sln text eol=crlf +# Declare files that will always have LF line endings on checkout. +build text eol=lf +mvnw text eol=lf + # Denote all files that are truly binary and should not be modified. *.gif binary *.jar binary diff --git a/Jenkinsfile b/Jenkinsfile index f738f7f726..c70bacada5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -205,8 +205,8 @@ timestamps { -DskipFuncTests \ -DskipArqTests \ -Dmaven.test.failure.ignore \ + -Dvictims \ """ - // TODO add -Dvictims def surefireTestReports = 'target/surefire-reports/TEST-*.xml' diff --git a/LICENSE.LESSER.txt b/LICENSE.LESSER.txt deleted file mode 100644 index 551cb4acb9..0000000000 --- a/LICENSE.LESSER.txt +++ /dev/null @@ -1,502 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -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 this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -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 -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser 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 Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "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 -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY 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 -LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey 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 library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index d159169d10..0000000000 --- a/LICENSE.txt +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) 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 -this service 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 make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. 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. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -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 -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the 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 a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE 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. - - 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 -convey 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 2 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, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision 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, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This 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. diff --git a/api/zanata-common-api/src/main/java/org/zanata/common/EntityStatus.java b/api/zanata-common-api/src/main/java/org/zanata/common/EntityStatus.java index 62c1952196..77ba53e64d 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/common/EntityStatus.java +++ b/api/zanata-common-api/src/main/java/org/zanata/common/EntityStatus.java @@ -20,13 +20,24 @@ */ package org.zanata.common; +import com.webcohesion.enunciate.metadata.Label; + import javax.xml.bind.annotation.XmlEnum; import javax.xml.bind.annotation.XmlType; +/** + * The possible state of various entities in the system. + */ @XmlType(name = "entityStatusType") @XmlEnum(String.class) +@Label("Status") public enum EntityStatus { - ACTIVE("jsf.Active"), READONLY("jsf.ReadOnly"), OBSOLETE("jsf.Obsolete"); + /** Regular state for most entities. Means it is readable and writeable */ + ACTIVE("jsf.Active"), + /** Same as active, but cannot be modified */ + READONLY("jsf.ReadOnly"), + /** Removed from the system. An object in this state cannot be accessed. */ + OBSOLETE("jsf.Obsolete"); public static EntityStatus valueOf(char initial) { switch (initial) { diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/RestUtil.java b/api/zanata-common-api/src/main/java/org/zanata/rest/RestUtil.java index 521199db32..ac63dfe8c2 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/RestUtil.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/RestUtil.java @@ -1,13 +1,24 @@ package org.zanata.rest; +import javax.ws.rs.core.Response; + public class RestUtil { - public static String convertToDocumentURIId(String id) { + public static String convertToDocumentURIId(String docId) { // NB this currently prevents us from allowing ',' in file names - if (id.startsWith("/")) { - return id.substring(1).replace('/', ','); + if (docId.startsWith("/")) { + return docId.substring(1).replace('/', ','); } - return id.replace('/', ','); + return docId.replace('/', ','); + } + + public static String convertFromDocumentURIId(String docIdWithNoSlash) { + return docIdWithNoSlash.replace(',', '/'); + } + + public static boolean isNotFound(Response response) { + return response.getStatus() == + Response.Status.NOT_FOUND.getStatusCode(); } } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/CopyTransStatus.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/CopyTransStatus.java index 6380218b77..3b4a46e2e4 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/CopyTransStatus.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/CopyTransStatus.java @@ -20,6 +20,9 @@ */ package org.zanata.rest.dto; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; @@ -32,12 +35,17 @@ */ @XmlRootElement(name = "copyTransStatus") @XmlType(name = "copyTransStatusType") +@Label("Copy Trans Status") public class CopyTransStatus { private int percentageComplete; private boolean inProgress; + /** + * An estimated percentage of completion for the copy trans run. + */ @XmlElement(required = true) + @DocumentationExample("80") public int getPercentageComplete() { return percentageComplete; } @@ -46,6 +54,9 @@ public void setPercentageComplete(int percentageComplete) { this.percentageComplete = percentageComplete; } + /** + * True if the process is still running. False otherwise + */ @XmlElement(required = true) public boolean isInProgress() { return inProgress; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryEntry.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryEntry.java index 9801098371..230cdcc2d3 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryEntry.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryEntry.java @@ -30,6 +30,8 @@ import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; @@ -39,7 +41,8 @@ import org.zanata.rest.MediaTypes; /** - * + * A single glossary entry representing a single translated term in multiple + * locales. * @author Alex Eng aeng@redhat.com * **/ @@ -48,6 +51,7 @@ "description", "sourceReference", "glossaryTerms", "termsCount", "qualifiedName" }) @JsonPropertyOrder({ "id", "pos", "description", "srcLang", "sourceReference", "glossaryTerms", "termsCount", "qualifiedName" }) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Glossary Entry") public class GlossaryEntry implements Serializable, HasMediaType { /** * @@ -78,8 +82,12 @@ public GlossaryEntry(Long id) { this.id = id; } + /** + * Unique identifier + */ @XmlElement(name = "id", namespace = Namespaces.ZANATA_OLD) @JsonProperty("id") + @DocumentationExample(value = "444555", value2 = "444556") public Long getId() { return id; } @@ -88,8 +96,12 @@ public void setId(Long id) { this.id = id; } + /** + * Glossary entry's part of speech + */ @XmlElement(name = "pos", namespace = Namespaces.ZANATA_OLD) @JsonProperty("pos") + @DocumentationExample(value = "verb", value2 = "noun") public String getPos() { return pos; } @@ -108,8 +120,13 @@ public void setDescription(String description) { this.description = description; } + /** + * Number of translated terms. A term is the glossary entry's representation + * for a specific locale + */ @XmlElement(name = "termsCount", namespace = Namespaces.ZANATA_API) @JsonProperty("termsCount") + @DocumentationExample("2") public int getTermsCount() { return termsCount; } @@ -118,6 +135,9 @@ public void setTermsCount(int termsCount) { this.termsCount = termsCount; } + /** + * The full list of glossary terms + */ @XmlElement(name = "glossary-term", namespace = Namespaces.ZANATA_OLD) @JsonProperty("glossaryTerms") public List getGlossaryTerms() { @@ -131,9 +151,13 @@ public void setGlossaryTerms(List glossaryTerms) { this.glossaryTerms = glossaryTerms; } + /** + * The source locale for this specific entry + */ @XmlAttribute(name = "src-lang") @XmlJavaTypeAdapter(type = LocaleId.class, value = LocaleIdAdapter.class) @JsonProperty("srcLang") + @DocumentationExample("en-US") public LocaleId getSrcLang() { return srcLang; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryInfo.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryInfo.java index a100494b4b..30721f72b3 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryInfo.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryInfo.java @@ -7,12 +7,14 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.zanata.common.Namespaces; /** + * Information about a specific Glossary. * @author Alex Eng aeng@redhat.com */ @XmlRootElement(name = "glossaryInfo") @@ -20,6 +22,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonPropertyOrder({ "srcLocale", "transLocale"}) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Glossary Info") public class GlossaryInfo implements Serializable { private static final long serialVersionUID = -5688873815049369490L; private GlossaryLocaleInfo srcLocale; @@ -35,6 +38,9 @@ public GlossaryInfo(GlossaryLocaleInfo srcLocale, this.transLocale = transLocale; } + /** + * The glossary's source locale + */ @XmlElement(name = "srcLocale", required = false, namespace = Namespaces.ZANATA_API) public GlossaryLocaleInfo getSrcLocale() { @@ -45,6 +51,9 @@ public void setSrcLocale(GlossaryLocaleInfo srcLocale) { this.srcLocale = srcLocale; } + /** + * The list of translated locale's available for the glossary + */ @XmlElement(name = "transLocale", required = false, namespace = Namespaces.ZANATA_API) public List getTransLocale() { diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryLocaleInfo.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryLocaleInfo.java index 9955495933..458a932b19 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryLocaleInfo.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryLocaleInfo.java @@ -5,6 +5,8 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; @@ -18,6 +20,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonPropertyOrder({ "locale", "numberOfTerms"}) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Glossary Locale Info") public class GlossaryLocaleInfo implements Serializable { private static final long serialVersionUID = 7486128063191358182L; private LocaleDetails locale; @@ -42,8 +45,12 @@ public void setLocale(LocaleDetails locale) { this.locale = locale; } + /** + * Number of terms available for the glossary in this locale + */ @XmlElement(name = "numberOfTerms", required = false, namespace = Namespaces.ZANATA_API) + @DocumentationExample("2") public int getNumberOfTerms() { return numberOfTerms; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryResults.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryResults.java index f67a021a3e..8aa66745cb 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryResults.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryResults.java @@ -10,6 +10,7 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; @@ -17,8 +18,8 @@ import org.zanata.common.Namespaces; /** - * Wrapper for list of HGlossaryEntry/GlossaryEntry and list of warning message after - * saving/update + * Wrapper for list of Glossary entries and a list of warning messages after + * saving/updating * * @author Alex Eng aeng@redhat.com */ @@ -27,6 +28,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) @XmlType(name = "glossaryResults", propOrder = {"glossaryEntries", "warnings"}) +@Label("Glossary Results") public class GlossaryResults implements Serializable { private static final long serialVersionUID = 7100495681284134288L; private List glossaryEntries; @@ -40,6 +42,9 @@ public GlossaryResults(List glossaryEntries, List warning this.warnings = warnings; } + /** + * The list of created / updated glossary entries + */ @JsonProperty("glossaryEntries") @XmlElementWrapper(name = "glossaryEntries", namespace = Namespaces.ZANATA_API) @XmlElementRef(namespace = Namespaces.ZANATA_API) @@ -50,6 +55,9 @@ public List getGlossaryEntries() { return glossaryEntries; } + /** + * A list of warnings generated when performing the operation + */ @JsonProperty("warnings") @XmlElementWrapper(name = "warnings", namespace = Namespaces.ZANATA_API) public List getWarnings() { diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryTerm.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryTerm.java index 82798e70df..079825dcc1 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryTerm.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/GlossaryTerm.java @@ -29,6 +29,8 @@ import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; @@ -37,15 +39,15 @@ import org.zanata.common.Namespaces; /** - * + * A single glossary term for a single locale * @author Alex Eng aeng@redhat.com * - **/ - + */ @XmlType(name = "glossaryTermType", propOrder = {"comment", "content", "locale", "lastModifiedDate", "lastModifiedBy"}) @JsonPropertyOrder({ "content", "comment", "locale", "lastModifiedDate", "lastModifiedBy" }) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Glossary Term") public class GlossaryTerm implements Serializable { /** * @@ -66,9 +68,13 @@ public class GlossaryTerm implements Serializable { public GlossaryTerm() { } + /** + * Term's locale + */ @XmlAttribute(name = "lang", namespace = Namespaces.XML) @XmlJavaTypeAdapter(type = LocaleId.class, value = LocaleIdAdapter.class) @JsonProperty("locale") + @DocumentationExample(value = "es-ES", value2 = "ja") public LocaleId getLocale() { return locale; } @@ -77,9 +83,13 @@ public void setLocale(LocaleId locale) { this.locale = locale; } + /** + * The term's translation in the given locale + */ @XmlElement(name = "content", required = false, namespace = Namespaces.ZANATA_OLD) @JsonProperty("content") + @DocumentationExample(value = "Una casa", value2 = "家") public String getContent() { return content; } @@ -98,9 +108,13 @@ public void setComment(String comment) { this.comment = comment; } + /** + * A string which identifies the user who last modififed this entry + */ @XmlElement(name = "lastModifiedBy", required = false, namespace = Namespaces.ZANATA_API) @JsonProperty("lastModifiedBy") + @DocumentationExample("homer") public String getLastModifiedBy() { return lastModifiedBy; } @@ -109,6 +123,9 @@ public void setLastModifiedBy(String lastModifiedBy) { this.lastModifiedBy = lastModifiedBy; } + /** + * A timestamp indicating the last modification date for this entry + */ @XmlElement(name = "lastModifiedDate", required = false, namespace = Namespaces.ZANATA_API) @JsonProperty("lastModifiedDate") diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Link.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Link.java index 920e0f28ff..778ca9814d 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Link.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Link.java @@ -1,11 +1,18 @@ package org.zanata.rest.dto; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; + import java.net.URI; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; +/** + * A single link to reference a URI + */ @XmlType(name = "linkType") +@Label("Link") public class Link { private URI href; @@ -30,7 +37,11 @@ public Link(URI href, String rel, String type) { this.type = type; } + /** + * The URI reference by this link + */ @XmlAttribute(name = "href", required = true) + @DocumentationExample(value = "http://alink.com") public URI getHref() { return href; } @@ -39,6 +50,9 @@ public void setHref(URI href) { this.href = href; } + /** + * The relationship this link holds to its parent object + */ @XmlAttribute(name = "rel", required = false) public String getRel() { return rel; @@ -48,6 +62,9 @@ public void setRel(String rel) { this.rel = rel; } + /** + * The type of link + */ @XmlAttribute(name = "type", required = true) public String getType() { return type; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Links.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Links.java index e45b310a64..8f50ea375e 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Links.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Links.java @@ -1,11 +1,17 @@ package org.zanata.rest.dto; +import com.webcohesion.enunciate.metadata.Label; + import java.util.ArrayList; import java.util.List; import javax.xml.bind.annotation.XmlType; +/** + * A collection of links + */ @XmlType(name = "linksType", propOrder = {}) +@Label("Links") public class Links extends ArrayList { private static final long serialVersionUID = 1L; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/LocaleDetails.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/LocaleDetails.java index a29b9770bb..28859b8de1 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/LocaleDetails.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/LocaleDetails.java @@ -29,6 +29,8 @@ import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; @@ -40,6 +42,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonPropertyOrder({"localeId", "displayName", "alias", "nativeName", "enabled", "enabledByDefault", "pluralForms"}) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Locale Details") public class LocaleDetails implements Serializable { private static final long serialVersionUID = -8133147543880728788L; @@ -68,9 +71,13 @@ public LocaleDetails(LocaleId localeId, String displayName, String alias, this.pluralForms = pluralForms; } + /** + * Unique locale identifier + */ @XmlAttribute(name = "localeId", required = true) @XmlJavaTypeAdapter(type = LocaleId.class, value = LocaleIdAdapter.class) @NotNull + @DocumentationExample(value = "es-ES", value2 = "ja") public LocaleId getLocaleId() { return localeId; } @@ -79,7 +86,11 @@ public void setLocaleId(LocaleId localeId) { this.localeId = localeId; } + /** + * Locale's display name (in English) + */ @XmlAttribute(name = "displayName", required = true) + @DocumentationExample(value = "Spanish (Spain)", value2 = "Japanese") public String getDisplayName() { return displayName; } @@ -88,7 +99,11 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } + /** + * An alternative name (if present) for this locale + */ @XmlAttribute(name = "alias", required = false) + @DocumentationExample(value = "es", value2 = "ja-JP") public String getAlias() { return alias; } @@ -98,6 +113,7 @@ public void setAlias(String alias) { } @XmlAttribute(name = "nativeName", required = false) + @DocumentationExample(value = "Español", value2 = "日本語") public String getNativeName() { return nativeName; } @@ -106,6 +122,9 @@ public void setNativeName(String nativeName) { this.nativeName = nativeName; } + /** + * Indicates whether the locale is enabled in the system or not. + */ @XmlAttribute(name = "enabled", required = true) @NotNull public boolean isEnabled() { @@ -116,6 +135,11 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + /** + * Indicates whether the locale will be used automatically by the system. + * e.g. when creating a new project, 'enabled by default' locales will + * automatically be added to the project unless specifically indicating so. + */ @XmlAttribute(name = "enabledByDefault", required = true) @NotNull public boolean isEnabledByDefault() { @@ -126,7 +150,12 @@ public void setEnabledByDefault(boolean enabledByDefault) { this.enabledByDefault = enabledByDefault; } + /** + * A string describing the formula for the locale's plural forms + */ @XmlAttribute(name = "pluralForms", required = false) + @DocumentationExample(value = "nplurals=2; plural=(n != 1)", + value2 = "nplurals=1; plural=0") public String getPluralForms() { return pluralForms; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProcessStatus.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProcessStatus.java index 143c051013..41f8993548 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProcessStatus.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProcessStatus.java @@ -65,6 +65,10 @@ public enum ProcessStatusCode { /** The process has finshed with a failure */ @XmlEnumValue("Failed") Failed, + + /** The process has been cancelled */ + @XmlEnumValue("Cancelled") + Cancelled } private String url; @@ -111,6 +115,11 @@ public void setMessages(List messages) { this.messages = messages; } + public ProcessStatus addMessage(String message) { + getMessages().add(message); + return this; + } + @XmlElement public ProcessStatusCode getStatusCode() { return statusCode; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Project.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Project.java index 4d13b427f3..227f0740ee 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Project.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/Project.java @@ -13,6 +13,8 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; @@ -40,6 +42,7 @@ "sourceViewURL", "sourceCheckoutURL", "links", "iterations", "status" }) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = Inclusion.NON_NULL) +@Label("Project") public class Project implements Serializable, HasCollectionSample, HasMediaType { @@ -70,7 +73,11 @@ public Project(String id, String name, String type, String description) { this.description = description; } + /** + * The project identifier (slug) + */ @XmlAttribute(name = "id", required = true) + @DocumentationExample("my-project") public String getId() { return id; } @@ -79,8 +86,12 @@ public void setId(String id) { this.id = id; } + /** + * The project type. + */ @XmlElement(name = "defaultType", required = true, nillable = false, namespace = Namespaces.ZANATA_OLD) + @DocumentationExample("Gettext") public String getDefaultType() { return defaultType; } @@ -93,6 +104,7 @@ public void setDefaultType(String defaultType) { @Size(max = 80) @XmlElement(name = "name", required = true, namespace = Namespaces.ZANATA_OLD) + @DocumentationExample("My Project") public String getName() { return name; } @@ -101,9 +113,10 @@ public void setName(String name) { this.name = name; } - @Size(max = 80) + @Size(max = 100) @XmlElement(name = "description", required = false, namespace = Namespaces.ZANATA_OLD) + @DocumentationExample("This is a sample project.") public String getDescription() { return description; } @@ -112,8 +125,12 @@ public void setDescription(String description) { this.description = description; } + /** + * The url to view the project's sources. + */ @XmlElement(name = "sourceViewURL", required = false, nillable = true, namespace = Namespaces.ZANATA_API) + @DocumentationExample("http://source.view.com") public String getSourceViewURL() { return sourceViewURL; } @@ -122,8 +139,12 @@ public void setSourceViewURL(String sourceViewURL) { this.sourceViewURL = sourceViewURL; } + /** + * The url where to checkout the project's sources. + */ @XmlElement(name = "sourceCheckoutURL", required = false, nillable = true, namespace = Namespaces.ZANATA_API) + @DocumentationExample("http://source.checkout.com") public String getSourceCheckoutURL() { return sourceCheckoutURL; } @@ -132,6 +153,13 @@ public void setSourceCheckoutURL(String sourceCheckoutURL) { this.sourceCheckoutURL = sourceCheckoutURL; } + /** + * Set of links managed by this project + * + * This field is ignored in PUT/POST operations + * + * @return set of Links managed by this resource + */ @XmlElement(name = "link", namespace = Namespaces.ZANATA_API) @JsonProperty("links") public Links getLinks() { @@ -149,6 +177,9 @@ public Links getLinks(boolean createIfNull) { return links; } + /** + * A list of versions (iterations) in the project + */ @XmlElementWrapper(name = "project-iterations", namespace = Namespaces.ZANATA_OLD) @XmlElementRef(namespace = Namespaces.ZANATA_OLD) @@ -167,6 +198,9 @@ public List getIterations(boolean createIfNull) { return getIterations(); } + /** + * System state of the project + */ @XmlElement(name = "status", required = false, namespace = Namespaces.ZANATA_OLD) public EntityStatus getStatus() { diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProjectIteration.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProjectIteration.java index 50461a6ab5..eba53b691e 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProjectIteration.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ProjectIteration.java @@ -29,6 +29,8 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; @@ -39,12 +41,16 @@ import org.zanata.rest.MediaTypes; import org.zanata.rest.MediaTypes.Format; +/** + * Represents a Project version (or iteration). + */ @XmlType(name = "projectIterationType", propOrder = { "links", "status", "projectType" }) @XmlRootElement(name = "project-iteration") @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = Inclusion.NON_NULL) @JsonPropertyOrder({ "id", "links", "status", "projectType" }) +@Label("Project Version") public class ProjectIteration implements Serializable, HasCollectionSample, HasMediaType { @@ -64,7 +70,11 @@ public ProjectIteration(String id) { this.id = id; } + /** + * Version identifier (slug) + */ @XmlAttribute(name = "id", required = true) + @DocumentationExample("my-iteration") public String getId() { return id; } @@ -107,8 +117,13 @@ public void setStatus(EntityStatus status) { this.status = status; } + /** + * The type of project version. This type could be overriding the project's + * original type. + */ @XmlElement(name = "projectType", required = false, namespace = Namespaces.ZANATA_OLD) + @DocumentationExample("Podir") public String getProjectType() { return projectType; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/QualifiedName.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/QualifiedName.java index cbf04aa5af..7101eb832c 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/QualifiedName.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/QualifiedName.java @@ -4,6 +4,8 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; @@ -11,7 +13,7 @@ import org.zanata.rest.MediaTypes; /** - * Object Glossary qualified name. Usage: + * Describes a qualified system name. Usage: * {@link GlossaryEntry#getQualifiedName()} * {@link org.zanata.rest.service.GlossaryResource} * @@ -20,6 +22,7 @@ @XmlRootElement(name = "qualifiedName") @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Qualified Name") public class QualifiedName implements Serializable, HasMediaType { private static final long serialVersionUID = 934216980812012602L; private String name; @@ -34,6 +37,7 @@ public QualifiedName(String name) { @XmlElement(name = "name", required = false, namespace = Namespaces.ZANATA_API) @JsonProperty("name") + @DocumentationExample("global/default") public String getName() { return name; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ResultList.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ResultList.java index 7a705ecc37..b39842c846 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ResultList.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/ResultList.java @@ -4,16 +4,19 @@ import java.util.ArrayList; import java.util.List; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; /** + * A list of results * @author Alex Eng aeng@redhat.com */ @JsonPropertyOrder({ "results", "totalCount" }) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Result List") public class ResultList implements Serializable { private static final long serialVersionUID = -2149554068631922866L; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/SearchResult.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/SearchResult.java index eaf12f0111..5a1431ab2e 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/SearchResult.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/SearchResult.java @@ -38,7 +38,8 @@ public enum SearchResultType { Project, LanguageTeam, Person, - Group; + Group, + ProjectVersion } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/VersionInfo.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/VersionInfo.java index 2f099ecd83..b5182d9812 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/VersionInfo.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/VersionInfo.java @@ -6,19 +6,22 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonTypeName; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.zanata.common.Namespaces; /** - * Holds version info + * Holds system version information */ @XmlRootElement(name = "versionInfo") @XmlType(name = "versionType", propOrder = { "versionNo", "buildTimeStamp", "scmDescribe" }) @JsonTypeName(value = "versionType") @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Version Info") public final class VersionInfo implements Serializable { private static final long serialVersionUID = 1L; private String versionNo; @@ -43,16 +46,27 @@ public VersionInfo(VersionInfo other) { this(other.versionNo, other.buildTimeStamp, other.scmDescribe); } + /** + * Version number + */ @XmlElement(name = "versionNo", namespace = Namespaces.ZANATA_OLD) + @DocumentationExample("4.0.0") public String getVersionNo() { return versionNo; } + /** + * ISO8601 timestamp for when the current system version was built + */ @XmlElement(name = "buildTimeStamp", namespace = Namespaces.ZANATA_OLD) + @DocumentationExample("20170225-1448") public String getBuildTimeStamp() { return buildTimeStamp; } + /** + * Identifier for the current version in source control + */ @XmlElement(name = "scmDescribe", namespace = Namespaces.ZANATA_API) public String getScmDescribe() { return scmDescribe; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/Resource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/Resource.java index 4a7b63192b..047ed55dc6 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/Resource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/Resource.java @@ -1,21 +1,19 @@ package org.zanata.rest.dto.resource; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlType; - -import com.webcohesion.enunciate.metadata.DocumentationExample; -import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.zanata.common.Namespaces; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + /** * A series of text flows to be translated and sharing common metadata. */ @@ -24,7 +22,7 @@ @JsonPropertyOrder({ "name", "contentType", "lang", "extensions", "textFlows" }) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) -@ResourceLabel("Resource") +@Label("Resource") public class Resource extends AbstractResourceMeta { private static final long serialVersionUID = 1L; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/TranslationsResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/TranslationsResource.java index c90fdce691..8a4a98f8e8 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/TranslationsResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/resource/TranslationsResource.java @@ -10,6 +10,7 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; @@ -31,6 +32,7 @@ @JsonPropertyOrder({ "links", "extensions", "textFlowTargets" }) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Label("Translations") public class TranslationsResource implements Serializable, HasSample, Extensible { @@ -40,6 +42,9 @@ public class TranslationsResource implements Serializable, private List textFlowTargets; private Integer revision; + /** + * Any provided extensions + */ @XmlElementWrapper(name = "extensions", required = false, namespace = Namespaces.ZANATA_OLD) @XmlElement(name = "extension", namespace = Namespaces.ZANATA_OLD) @@ -60,6 +65,9 @@ public ExtensionSet getExtensions( return extensions; } + /** + * The text flow targets (i.e. translated text) + */ @XmlElementWrapper(name = "targets", required = false, namespace = Namespaces.ZANATA_OLD) @XmlElement(name = "text-flow-target", namespace = Namespaces.ZANATA_API) @@ -71,6 +79,9 @@ public List getTextFlowTargets() { return textFlowTargets; } + /** + * A collection of links provided with the translations. + */ @XmlElement(name = "links", namespace = Namespaces.ZANATA_OLD) public Links getLinks() { return links; @@ -87,6 +98,9 @@ public Links getLinks(boolean createIfNull) { return links; } + /** + * Revision number for the translations + */ @XmlAttribute() public Integer getRevision() { return revision; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/ContainerTranslationStatistics.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/ContainerTranslationStatistics.java index 1b38fbbc90..97cb9c0562 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/ContainerTranslationStatistics.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/ContainerTranslationStatistics.java @@ -29,6 +29,8 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.zanata.rest.dto.Link; @@ -45,6 +47,7 @@ @XmlRootElement(name = "containerStats") @JsonIgnoreProperties(ignoreUnknown = true) @JsonPropertyOrder({ "id", "refs", "stats", "detailedStats" }) +@Label("Container Translation Statistics") public class ContainerTranslationStatistics implements Serializable { private static final long serialVersionUID = 1L; private String id; @@ -60,6 +63,7 @@ public ContainerTranslationStatistics() { * etc). */ @XmlAttribute + @DocumentationExample("my-project") public String getId() { return id; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/TranslationStatistics.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/TranslationStatistics.java index f029125bf5..8f47fb3b82 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/TranslationStatistics.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/TranslationStatistics.java @@ -29,6 +29,8 @@ import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonPropertyOrder; @@ -53,6 +55,7 @@ @JsonPropertyOrder({ "total", "untranslated", "needReview", "translated", "approved", "rejected", "readyForReview", "fuzzy", "unit", "locale", "lastTranslated" }) +@Label("Translation Statistics") public class TranslationStatistics implements Serializable { private static final long serialVersionUID = 1L; private StatUnit unit; @@ -111,6 +114,7 @@ public TranslationStatistics(TransUnitWords wordCount, String locale) { * Number of untranslated elements. */ @XmlAttribute + @DocumentationExample("25") public long getUntranslated() { return translationCount.getUntranslated(); } @@ -147,6 +151,7 @@ public long getNeedReview() { * @return */ @XmlAttribute + @DocumentationExample("50") public long getFuzzy() { return translationCount.getNeedReview(); } @@ -179,6 +184,7 @@ public long getTranslatedAndApproved() { * @return number of translated but not yet approved elements. */ @XmlAttribute + @DocumentationExample("30") public long getTranslatedOnly() { return translationCount.getTranslated(); } @@ -191,6 +197,7 @@ public void setTranslatedOnly(long translatedOnly) { * @return Number of approved elements. */ @XmlAttribute + @DocumentationExample("80") public long getApproved() { return translationCount.getApproved(); } @@ -200,6 +207,7 @@ public void setApproved(long approved) { } @XmlAttribute + @DocumentationExample("10") public long getRejected() { return translationCount.getRejected(); } @@ -242,6 +250,7 @@ public void setUnit(StatUnit unit) { * Locale for the translation statistics. */ @XmlAttribute + @DocumentationExample("es-ES") public String getLocale() { return locale; } @@ -250,7 +259,11 @@ public void setLocale(String locale) { this.locale = locale; } + /** + * Last translation information. Includes date and user. + */ @XmlAttribute + @DocumentationExample("31/12/15 23:59 by homer") public String getLastTranslated() { return lastTranslated; } @@ -341,6 +354,7 @@ public String toString() { return sb.toString(); } + @Label("Stats Unit") public enum StatUnit { /** Statistics are measured in words. */ WORD, diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/BaseContributionStatistic.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/BaseContributionStatistic.java index 8ea3827ac2..615b07da74 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/BaseContributionStatistic.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/BaseContributionStatistic.java @@ -1,5 +1,6 @@ package org.zanata.rest.dto.stats.contribution; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; @@ -15,6 +16,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) @JsonPropertyOrder({ "approved", "rejected", "translated", "needReview" }) +@Label("Base Contribution Statistics") public class BaseContributionStatistic implements Serializable { private static final long serialVersionUID = 6615374806881888982L; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/ContributionStatistics.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/ContributionStatistics.java index dd34f696bb..dec1e83020 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/ContributionStatistics.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/ContributionStatistics.java @@ -4,15 +4,20 @@ import java.util.ArrayList; import java.util.List; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; /** + * Contains information about translations contributed by a single user. + * * @author Alex Eng aeng@redhat.com */ @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) @JsonPropertyOrder({ "username", "contributions" }) +@Label("Contribution Statistics") public class ContributionStatistics implements Serializable { private static final long serialVersionUID = -6328249224235406066L; @@ -29,7 +34,11 @@ public ContributionStatistics(String username, this.contributions = contributions; } + /** + * User name responsible for the contributions. + */ @JsonProperty("username") + @DocumentationExample("bart") public String getUsername() { return username; } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/LocaleStatistics.java b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/LocaleStatistics.java index 60a38b7a8e..da99d8ec06 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/LocaleStatistics.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/dto/stats/contribution/LocaleStatistics.java @@ -2,16 +2,21 @@ import java.io.Serializable; +import com.webcohesion.enunciate.metadata.DocumentationExample; +import com.webcohesion.enunciate.metadata.Label; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.zanata.common.LocaleId; /** + * Contains translation statistics for a single locale. + * * @author Alex Eng aeng@redhat.com */ @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) @JsonPropertyOrder({ "locale", "translation-stats", "review-stats" }) +@Label("Locale Statistics") public class LocaleStatistics implements Serializable { private static final long serialVersionUID = 711345550950903773L; @@ -36,16 +41,26 @@ public LocaleStatistics(LocaleId locale, private BaseContributionStatistic reviewStats; + /** + * Locale code for for the stats + */ @JsonProperty("locale") + @DocumentationExample(value = "es-ES", value2 = "ja") public LocaleId getLocale() { return locale; } + /** + * Contains translation statistics only. + */ @JsonProperty("translation-stats") public BaseContributionStatistic getTranslationStats() { return translationStats; } + /** + * Contains review statistics only. + */ @JsonProperty("review-stats") public BaseContributionStatistic getReviewStats() { return reviewStats; diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/AsynchronousProcessResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/AsynchronousProcessResource.java index 9787da262e..1a9bff5ce9 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/AsynchronousProcessResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/AsynchronousProcessResource.java @@ -34,6 +34,8 @@ import javax.ws.rs.core.MediaType; import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.zanata.common.LocaleId; import org.zanata.rest.dto.ProcessStatus; import org.zanata.rest.dto.resource.Resource; @@ -52,6 +54,10 @@ @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @ResourceLabel("Asynchronous Process") +@StatusCodes({ + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") +}) public interface AsynchronousProcessResource extends RestResource { public static final String SERVICE_PATH = "/async"; @@ -79,18 +85,16 @@ public interface AsynchronousProcessResource extends RestResource { * Boolean value that indicates whether reasonably close * translations from other projects should be found to initially * populate this document's translations. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - The contents of the response will indicate the process - * identifier which may be used to query for its status or a message - * indicating what happened.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @POST @Path("/projects/p/{projectSlug}/iterations/i/{iterationSlug}/r") /* Same as SourceDocResourceService.SERVICE_PATH */ @TypeHint(ProcessStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The contents of the response will indicate the process" + + " identifier which may be used to query for its status or a message" + + " indicating what happened.") + }) @Deprecated public ProcessStatus startSourceDocCreation( @PathParam("id") String idNoSlash, @@ -123,19 +127,20 @@ public ProcessStatus startSourceDocCreation( * Boolean value that indicates whether reasonably close * translations from other projects should be found to initially * populate this document's translations. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - The contents of the response will indicate the process - * identifier which may be used to query for its status or a message - * indicating what happened.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * + * Deprecated. Use {@link #startSourceDocCreationOrUpdateWithDocId} */ + @Deprecated @PUT - @Path("/projects/p/{projectSlug}/iterations/i/{iterationSlug}/r" + @Path("/projects/p/{projectSlug}/iterations/i/{iterationSlug}" + SourceDocResource.RESOURCE_SLUG_TEMPLATE) /* Same as SourceDocResourceService.SERVICE_PATH */ @TypeHint(ProcessStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The contents of the response will indicate the process" + + " identifier which may be used to query for its status or a message" + + " indicating what happened.") + }) public ProcessStatus startSourceDocCreationOrUpdate( @PathParam("id") String idNoSlash, @PathParam("projectSlug") String projectSlug, @@ -143,6 +148,37 @@ public ProcessStatus startSourceDocCreationOrUpdate( Resource resource, @QueryParam("ext") Set extensions, @QueryParam("copyTrans") @DefaultValue("true") boolean copytrans); + /** + * Attempts to starts the creation or update of a source document. NOTE: + * Still experimental. + * + * @param docId + * The document identifier. + * @param projectSlug + * Project identifier. + * @param iterationSlug + * Project Iteration identifier. + * @param resource + * The document information. + * @param extensions + * The document extensions to save with the document (e.g. + * "gettext", "comment"). This parameter allows multiple values + * e.g. "ext=gettext&ext=comment". + */ + @PUT + @Path("/projects/p/{projectSlug}/iterations/i/{iterationSlug}/resource") + @TypeHint(ProcessStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The contents of the response will indicate the process" + + " identifier which may be used to query for its status or a message" + + " indicating what happened.") + }) + public ProcessStatus startSourceDocCreationOrUpdateWithDocId( + @PathParam("projectSlug") String projectSlug, + @PathParam("iterationSlug") String iterationSlug, + Resource resource, @QueryParam("ext") Set extensions, + @QueryParam("docId") @DefaultValue("") String docId); + /** * Attempts to start the translation of a document. NOTE: Still * experimental. @@ -175,18 +211,19 @@ public ProcessStatus startSourceDocCreationOrUpdate( * @param assignCreditToUploader * The translator field for all uploaded translations will * be set to the user who performs the upload. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - The contents of the response will indicate the process - * identifier which may be used to query for its status or a message - * indicating what happened.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * + * Deprecated. Use {@link #startTranslatedDocCreationOrUpdateWithDocId} */ + @Deprecated @PUT @Path("/projects/p/{projectSlug}/iterations/i/{iterationSlug}/r/{id}/translations/{locale}") /* Same as TranslatedDocResource.putTranslations */ @TypeHint(ProcessStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The contents of the response will indicate the process" + + " identifier which may be used to query for its status or a message" + + " indicating what happened.") + }) public ProcessStatus startTranslatedDocCreationOrUpdate( @PathParam("id") String idNoSlash, @@ -198,6 +235,54 @@ ProcessStatus startTranslatedDocCreationOrUpdate( @QueryParam("merge") String merge, @QueryParam("assignCreditToUploader") @DefaultValue("false") boolean assignCreditToUploader); + /** + * Attempts to start the translation of a document. NOTE: Still + * experimental. + * + * @param docId + * The document identifier. + * @param projectSlug + * Project identifier. + * @param iterationSlug + * Project Iteration identifier. + * @param locale + * The locale for which to get translations. + * @param translatedDoc + * The translations to modify. + * @param extensions + * The document extensions to save with the document (e.g. + * "gettext", "comment"). This parameter allows multiple values + * e.g. "ext=gettext&ext=comment". + * @param merge + * Indicates how to deal with existing translations (valid + * options: 'auto', 'import'). Import will overwrite all current + * values with the values being pushed (even empty ones), while + * Auto will check the history of your translations and will not + * overwrite any translations for which it detects a previous + * value is being pushed. + * @param assignCreditToUploader + * The translator field for all uploaded translations will + * be set to the user who performs the upload. + */ + @PUT + @Path("/projects/p/{projectSlug}/iterations/i/{iterationSlug}/resource/translations/{locale}") + /* Same as TranslatedDocResource.putTranslations */ + @TypeHint(ProcessStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The contents of the response will indicate the process" + + " identifier which may be used to query for its status or a message" + + " indicating what happened.") + }) + public ProcessStatus startTranslatedDocCreationOrUpdateWithDocId( + @PathParam("projectSlug") String projectSlug, + @PathParam("iterationSlug") String iterationSlug, + @PathParam("locale") LocaleId locale, + TranslationsResource translatedDoc, + @QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions, + @QueryParam("merge") String merge, + @QueryParam("assignCreditToUploader") @DefaultValue("false") boolean assignCreditToUploader); + /** * Obtains the status of a previously started process. * @@ -215,6 +300,12 @@ ProcessStatus startTranslatedDocCreationOrUpdate( @GET @Path("/{processId}") @TypeHint(ProcessStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = + "On normal circumstances. The response data will have" + + " all information about the status of the running process"), + @ResponseCode(code = 404, condition = "If such a process Id is not found on the server.") + }) public ProcessStatus getProcessStatus( @PathParam("processId") String processId); diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/CopyTransResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/CopyTransResource.java index 4138bcbbbf..70be790686 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/CopyTransResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/CopyTransResource.java @@ -29,6 +29,8 @@ import javax.ws.rs.core.MediaType; import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.zanata.rest.dto.CopyTransStatus; import com.webcohesion.enunciate.metadata.rs.TypeHint; @@ -55,26 +57,23 @@ public interface CopyTransResource extends RestResource { * Project version identifier * @param docId * Document Id to copy translations into. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Translation copy was started for the given document. - * The status of the process is also returned in the response - * contents.
- * UNAUTHORIZED(401) - If the user does not have the proper - * permissions to perform this operation.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. Translation copy will - * not start in this case. */ @POST @Path("/proj/{projectSlug}/iter/{iterationSlug}/doc/{docId:.+}") // /copytrans/proj/{projectSlug}/iter/{iterationSlug}/doc/{docId} - @TypeHint(CopyTransStatus.class) - public - CopyTransStatus startCopyTrans( - @PathParam("projectSlug") String projectSlug, - @PathParam("iterationSlug") String iterationSlug, - @PathParam("docId") String docId); + @TypeHint(CopyTransStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, + condition = "Translation copy was started for the given document. " + + "The status of the process is also returned in the response's body."), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) + public + CopyTransStatus startCopyTrans( + @PathParam("projectSlug") String projectSlug, + @PathParam("iterationSlug") String iterationSlug, + @PathParam("docId") String docId); /** * Retrieves the status for a Translation copy process for a document. @@ -85,22 +84,21 @@ CopyTransStatus startCopyTrans( * Project version identifier * @param docId * Document Id - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - A Translation copy process was found, and its status - * will be returned in the body of the response.
- * NOT_FOUND(404) - If there is no record of a recent translation - * copy process for the specified document. INTERNAL SERVER - * ERROR(500) - If there is an unexpected error in the server while - * performing this operation. */ @GET @Path("/proj/{projectSlug}/iter/{iterationSlug}/doc/{docId:.+}") // /copytrans/proj/{projectSlug}/iter/{iterationSlug}/doc/{docId} - @TypeHint(CopyTransStatus.class) - public - CopyTransStatus getCopyTransStatus( - @PathParam("projectSlug") String projectSlug, - @PathParam("iterationSlug") String iterationSlug, - @PathParam("docId") String docId); + @TypeHint(CopyTransStatus.class) + @StatusCodes({ + @ResponseCode(code = 200, + condition = "A translation copy process was found, and its " + + "status will be returned in the body of the response"), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) + public + CopyTransStatus getCopyTransStatus( + @PathParam("projectSlug") String projectSlug, + @PathParam("iterationSlug") String iterationSlug, + @PathParam("docId") String docId); } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/GlossaryResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/GlossaryResource.java index 0700c025e1..8f1f0e38cc 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/GlossaryResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/GlossaryResource.java @@ -37,6 +37,8 @@ import javax.ws.rs.core.Response; import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.jboss.resteasy.annotations.providers.multipart.MultipartForm; import org.zanata.common.LocaleId; import org.zanata.rest.GlossaryFileUploadForm; @@ -50,7 +52,7 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint; /** - * Glossary management via REST + * Glossary management * @author Alex Eng aeng@redhat.com * **/ @@ -63,6 +65,10 @@ MediaTypes.APPLICATION_ZANATA_GLOSSARY_XML, MediaTypes.APPLICATION_ZANATA_GLOSSARY_JSON }) @ResourceLabel("Glossary") +@StatusCodes({ + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") +}) public interface GlossaryResource extends RestResource { public static final String SERVICE_PATH = "/glossary"; @@ -78,13 +84,6 @@ public interface GlossaryResource extends RestResource { /** * Return default global glossary qualifiedName - * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - List of Global glossary qualified names used in the system. - * e.g {@link #GLOBAL_QUALIFIED_NAME} - * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @GET @Path("/qualifiedName") @@ -92,6 +91,10 @@ public interface GlossaryResource extends RestResource { MediaTypes.APPLICATION_ZANATA_GLOSSARY_JSON, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @TypeHint(QualifiedName.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Returns the qualified name " + + "for the system-wide glossary"), + }) public Response getQualifiedName(); /** @@ -99,12 +102,6 @@ public interface GlossaryResource extends RestResource { * * @param qualifiedName * Qualified name of glossary, default to {@link #GLOBAL_QUALIFIED_NAME} - * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Global glossary info in the system. - * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @GET @Path("/info") @@ -112,12 +109,19 @@ public interface GlossaryResource extends RestResource { MediaTypes.APPLICATION_ZANATA_GLOSSARY_JSON, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @TypeHint(GlossaryInfo.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Returns the global glossary " + + "information"), + }) public Response getInfo( @DefaultValue(GLOBAL_QUALIFIED_NAME) @QueryParam("qualifiedName") String qualifiedName); /** - * Returns Glossary entries for the given source and translation locale with - * paging + * Returns a subset of Glossary entries for the given source and translation + * locale as indicated by the paging parameters. + * + * @see {@link org.zanata.rest.dto.GlossaryEntry} for details on the result + * list's contents. * * @param srcLocale * Source locale - Required (default value: en-US). @@ -135,19 +139,18 @@ public Response getInfo( * See {@link org.zanata.common.GlossarySortField} * @param qualifiedName * Qualified name of glossary, default to {@link #GLOBAL_QUALIFIED_NAME} - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response containing all Glossary entries for the given - * locale. - * Bad request(400) - If page or sizePerPage is negative value, or sizePerPage is more than 1000. - * INTERNAL SERVER ERROR(500) - If there is an unexpected - * error in the server while performing this operation. */ @GET @Path("/entries") @Produces({ MediaTypes.APPLICATION_ZANATA_GLOSSARY_JSON, MediaType.APPLICATION_JSON }) @TypeHint(ResultList.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Response containing Glossary " + + "entries for the given locale."), + @ResponseCode(code = 400, condition = "If page or sizePerPage are " + + "negative, or sizePerPage is greater than 1000"), + }) public Response getEntries( @DefaultValue("en-US") @QueryParam("srcLocale") LocaleId srcLocale, @QueryParam("transLocale") LocaleId transLocale, @@ -160,6 +163,9 @@ public Response getEntries( /** * Returns Glossary entries based on a fuzzy text search. * + * @see {@link org.zanata.rest.dto.GlossaryEntry} for details on the result + * list's contents. + * * @param srcLocale * Source locale * @param transLocale @@ -172,23 +178,20 @@ public Response getEntries( * @param projectSlug * (optional) Project slug if a project glossary should be searched * in addition to the global glossary. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response containing all Glossary entries for the given - * locale. - * Bad request(400) - * - When maxResults is not strictly positive or is more than 1000. - * - When searchText is missing - * - When transLocale is missing - * - When there is an error parsing the searchText - * INTERNAL SERVER ERROR(500) - If there is an unexpected - * error in the server while performing this operation. */ @GET @Path("/search") @Produces({ MediaTypes.APPLICATION_ZANATA_GLOSSARY_JSON, MediaType.APPLICATION_JSON }) @TypeHint(ResultList.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Response containing Glossary " + + "entries for the given search parameters."), + @ResponseCode(code = 400, condition = "When maxResults is not strictly positive or is more than 1000"), + @ResponseCode(code = 400, condition = "When searchText is missing"), + @ResponseCode(code = 400, condition = "When transLocale is missing"), + @ResponseCode(code = 400, condition = "When there is an error parsing the searchText"), + }) Response search( @DefaultValue("en-US") @QueryParam("srcLocale") LocaleId srcLocale, @CheckForNull @QueryParam("transLocale") LocaleId transLocale, @@ -204,7 +207,7 @@ Response search( * @param locale include locale-specific detail for this locale * @param termIds id for glossary terms in the default locale, found in * results of {@link #search(LocaleId, LocaleId, int, String, String)} - * @return source and target glossary details. + * @return Source and target glossary details. */ @GET @Path("/details/{locale}") @@ -213,111 +216,122 @@ Response search( // TODO when GWT is removed, move the GlossaryDetails class to this module // and add the type hint. // @TypeHint(GlossaryDetails[].class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Details successfully found") + }) Response getDetails( @CheckForNull @PathParam("locale") LocaleId locale, @CheckForNull @QueryParam("termIds") List termIds); /** - * Download all glossary entries as file + * Download all glossary entries as a file * - * @param fileType - po or cvs (case insensitive). Default - csv - * @param locales - optional comma separated list of languages required. + * @param fileType 'po' or 'csv' (case insensitive) are currently supported + * @param locales optional comma separated list of languages required. * @param qualifiedName * Qualified name of glossary, default to {@link #GLOBAL_QUALIFIED_NAME} + * @return A file stream with the glossary contents in the specified format */ @GET @Path("/file") @Produces(MediaType.APPLICATION_OCTET_STREAM) + @StatusCodes({ + @ResponseCode(code = 200, condition = "File is successfully built " + + "and served") + }) public Response downloadFile( @DefaultValue("csv") @QueryParam("fileType") String fileType, @QueryParam("locales") String locales, @DefaultValue(GLOBAL_QUALIFIED_NAME) @QueryParam("qualifiedName") String qualifiedName); /** - * Create or update glossary entry. - * GlossaryTerm with locale different from {@param locale} will be ignored. + * Create or update glossary entries. + * Glossary Terms with a locale different from the given locale parameter + * will be ignored. * * @param glossaryEntries The glossary entries to create/update * @param qualifiedName - * Qualified name of glossary, default to {@link #GLOBAL_QUALIFIED_NAME} + * Qualified name of glossary, defaults to {@link #GLOBAL_QUALIFIED_NAME} * @param locale - * The translation locale to create/update - * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - If the glossary entry were successfully created/updated. - * UNAUTHORIZED(401) - If the user does not have the proper - * permissions to perform this operation.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * Locale to which the given glossary entries belong */ @POST @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Path("/entries") @TypeHint(GlossaryResults.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The glossary entries were " + + "successfully created or updated"), + @ResponseCode(code = 401, condition = "The user does not have the proper" + + " permissions to perform this operation") + }) public Response post(List glossaryEntries, @QueryParam("locale") String locale, @DefaultValue(GLOBAL_QUALIFIED_NAME) @QueryParam("qualifiedName") String qualifiedName); /** - * Upload glossary file (po, cvs) - * - * @param form {@link org.zanata.rest.GlossaryFileUploadForm} + * Upload glossary file (currently supported formats: po, csv) * - * @return The following response status codes will be returned from this - * operation:
- * CREATED(201) - If files successfully uploaded. - * UNAUTHORIZED(401) - If the user does not have the proper - * permissions to perform this operation.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. * + * @param form Multi-part form with the following named parts:
+ * file: The file contents
+ * srcLocale: Source locale for the glossary entries
+ * transLocale: Translation locale for the glossary entries
+ * fileName: The name of the file being uploaded
+ * qualifiedName: The qualified name for the glossary
*/ @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) @POST @TypeHint(GlossaryResults.class) + @StatusCodes({ + @ResponseCode(code = 201, condition = "Files successfully uploaded"), + @ResponseCode(code = 401, condition = "The user does not have the proper" + + " permissions to perform this operation") + }) public Response upload(@MultipartForm GlossaryFileUploadForm form); /** * - * Delete glossary which given id. + * Delete a glossary entry. * * @param id id for source glossary term * @param qualifiedName - * Qualified name of glossary, default to {@link #GLOBAL_QUALIFIED_NAME} - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - If the glossary entry were successfully deleted. - * UNAUTHORIZED(401) - If the user does not have the proper - * permissions to perform this operation.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * Qualified name of glossary, defaults to {@link #GLOBAL_QUALIFIED_NAME} + * @return the removed glossary entry */ @DELETE @Produces(MediaType.APPLICATION_JSON) @Path("/entries/{id}") @TypeHint(GlossaryEntry.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The glossary entry was successfully deleted"), + @ResponseCode(code = 401, condition = "The user does not have the proper" + + " permissions to perform this operation") + }) public Response deleteEntry(@PathParam("id") Long id, @DefaultValue(GLOBAL_QUALIFIED_NAME) @QueryParam("qualifiedName") String qualifiedName); /** - * Delete all glossary terms. + * Delete all entries in a glossary. * * @param qualifiedName - * Qualified name of glossary, default to {@link #GLOBAL_QUALIFIED_NAME} + * Qualified name of glossary, defaults to {@link #GLOBAL_QUALIFIED_NAME} + * + * @return The number of deleted glossary entries + * + * @responseExample * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - If the glossary entries were successfully deleted. - * UNAUTHORIZED(401) - If the user does not have the proper - * permissions to perform this operation.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * TODO Need to define a 'produces' header */ @DELETE @TypeHint(Integer.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "The glossary was deleted"), + @ResponseCode(code = 401, condition = "The user does not have the proper" + + " permissions to perform this operation") + }) public Response deleteAllEntries( @DefaultValue(GLOBAL_QUALIFIED_NAME) @QueryParam("qualifiedName") String qualifiedName); diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectIterationLocalesResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectIterationLocalesResource.java index 7db72a1ea9..9e5d51ae32 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectIterationLocalesResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectIterationLocalesResource.java @@ -53,6 +53,10 @@ public interface ProjectIterationLocalesResource extends RestResource { * * This may be the list of locales inherited from the project. * + * This also returns locale aliases. + * + * @see ProjectVersionResource#getLocales(String, String) + * * @return * OK 200 containing the list of LocaleDetails * NOT FOUND 404 if the project-version does not exist diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectVersionResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectVersionResource.java index c2e038b4de..d0066ec31e 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectVersionResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectVersionResource.java @@ -191,6 +191,8 @@ public Response getContributors( /** * Retrieves a full list of locales enabled in project version. * + * @see ProjectIterationLocalesResource#get() + * * @param projectSlug * Project identifier * @param versionSlug diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectsResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectsResource.java index 1f42452fd6..0f3e55a6fe 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectsResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/ProjectsResource.java @@ -29,6 +29,8 @@ import javax.ws.rs.core.Response; import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.zanata.rest.MediaTypes; import org.zanata.rest.dto.Project; @@ -49,22 +51,20 @@ public interface ProjectsResource extends RestResource { public static final String SERVICE_PATH = "/projects"; /** - * Retrieves a full list of projects in the system. The result is - * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response containing a full list of projects. The list - * will be wrapped in a "projects" element, and all its child - * elements will be "project"s.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * Retrieves a full list of projects in the system. */ @GET @Produces({ MediaTypes.APPLICATION_ZANATA_PROJECTS_XML, MediaTypes.APPLICATION_ZANATA_PROJECTS_JSON, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) - @TypeHint(Project[].class) - public - Response get(); + @TypeHint(Project[].class) + @StatusCodes({ + @ResponseCode(code = 200, + condition = "Response containing a full list of projects."), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) + public + Response get(); } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/SourceDocResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/SourceDocResource.java index 2a5561cc18..828f24d5b6 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/SourceDocResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/SourceDocResource.java @@ -58,15 +58,17 @@ @ResourceLabel("Source Documents") public interface SourceDocResource extends RestResource { @SuppressWarnings("deprecation") - String SERVICE_PATH = - ProjectIterationResource.SERVICE_PATH + "/r"; + String SERVICE_PATH = ProjectIterationResource.SERVICE_PATH; + String RESOURCE_PATH = "/r"; + String DOCID_RESOURCE_PATH = "/resource"; + String RESOURCE_SLUG_REGEX = "[\\-_a-zA-Z0-9]+([a-zA-Z0-9_\\-,{.}]*[a-zA-Z0-9]+)?"; String RESOURCE_NAME_REGEX = // as above, with ',' replaced by '/' "[\\-_a-zA-Z0-9]+([a-zA-Z0-9_\\-/{.}]*[a-zA-Z0-9]+)?"; - String RESOURCE_SLUG_TEMPLATE = "/{id:" - + RESOURCE_SLUG_REGEX + "}"; + String RESOURCE_SLUG_TEMPLATE = + RESOURCE_PATH + "/{id:" + RESOURCE_SLUG_REGEX + "}"; /** * Returns header information for a Project's iteration source strings. @@ -81,6 +83,7 @@ public interface SourceDocResource extends RestResource { * the server while performing this operation. */ @HEAD + @Path(RESOURCE_PATH) public Response head(); /** @@ -102,6 +105,7 @@ public interface SourceDocResource extends RestResource { * the server while performing this operation. */ @GET + @Path(RESOURCE_PATH) @TypeHint(ResourceMeta[].class) Response get(@QueryParam("ext") Set extensions); @@ -129,6 +133,7 @@ public interface SourceDocResource extends RestResource { * the server while performing this operation. */ @POST + @Path(RESOURCE_PATH) public Response post(Resource resource, @QueryParam("ext") Set extensions, @QueryParam("copyTrans") @DefaultValue("true") boolean copytrans); @@ -153,7 +158,9 @@ public Response post(Resource resource, * parameters.
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in * the server while performing this operation. + * Deprecated. Use {@link #getResourceWithDocId} */ + @Deprecated @GET @Path(RESOURCE_SLUG_TEMPLATE) // /r/{id} @@ -162,6 +169,30 @@ public Response post(Resource resource, Response getResource(@PathParam("id") String idNoSlash, @QueryParam("ext") Set extensions); + /** + * Retrieves information for a source Document. + * + * @param docId + * The document identifier. + * @param extensions + * The document extensions to fetch along with the document (e.g. + * "gettext", "comment"). This parameter allows multiple values + * e.g. "ext=gettext&ext=comment". + * @return The following response status codes will be returned from this + * operation:
+ * OK(200) - Response with the document's information.
+ * NOT FOUND(404) - If a document could not be found with the given + * parameters.
+ * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @GET + @TypeHint(Resource.class) + @Path(DOCID_RESOURCE_PATH) + public + Response getResourceWithDocId(@QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions); + /** * Creates or modifies a source Document. * @@ -194,7 +225,10 @@ Response getResource(@PathParam("id") String idNoSlash, * permissions to perform this operation.
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in * the server while performing this operation. + * + * Deprecated. Use {@link #putResourceWithDocId} */ + @Deprecated @PUT @Path(RESOURCE_SLUG_TEMPLATE) // /r/{id} @@ -206,6 +240,44 @@ Response getResource(@PathParam("id") String idNoSlash, @QueryParam("ext") Set extensions, @QueryParam("copyTrans") @DefaultValue("true") boolean copytrans); + + /** + * Creates or modifies a source Document. + * + * @param docId + * The document identifier. + * @param resource + * The document information. + * @param extensions + * The document extensions to save with the document (e.g. + * "gettext", "comment"). This parameter allows multiple values + * e.g. "ext=gettext&ext=comment". + * @param copytrans + * Boolean value that indicates whether reasonably close + * translations from other projects should be found to initially + * populate this document's translations. + * @return The following response status codes will be returned from this + * operation:
+ * CREATED(201) - If a new document was successfully created.
+ * OK(200) - If an already existing document was modified.
+ * NOT FOUND(404) - If a project or project iteration could not be + * found with the given parameters.
+ * FORBIDDEN(403) - If the user is not allowed to modify the + * project, project iteration or document. This might be due to the + * project or iteration being in Read-Only mode.
+ * UNAUTHORIZED(401) - If the user does not have the proper + * permissions to perform this operation.
+ * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @PUT + @Path(DOCID_RESOURCE_PATH) + public Response putResourceWithDocId( + Resource resource, + @QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions, + @QueryParam("copyTrans") @DefaultValue("true") boolean copytrans); + /** * Delete a source Document. The system keeps the history of this document however. * @@ -227,13 +299,39 @@ Response getResource(@PathParam("id") String idNoSlash, * permissions to perform this operation.
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in * the server while performing this operation. + * + * Deprecated. Use {@link #deleteResourceWithDocId} */ + @Deprecated @DELETE @Path(RESOURCE_SLUG_TEMPLATE) // /r/{id} public Response deleteResource(@PathParam("id") String idNoSlash); + /** + * Delete a source Document. The system keeps the history of this document however. + * + * @param docId + * The document identifier. + * @return The following response status codes will be returned from this + * operation:
+ * OK(200) - If The document was successfully deleted.
+ * NOT FOUND(404) - If a project or project iteration could not be + * found with the given parameters.
+ * FORBIDDEN(403) - If the user is not allowed to modify the + * project, project iteration or document. This might be due to the + * project or iteration being in Read-Only mode.
+ * UNAUTHORIZED(401) - If the user does not have the proper + * permissions to perform this operation.
+ * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @DELETE + @Path(DOCID_RESOURCE_PATH) + public Response deleteResourceWithDocId( + @QueryParam("docId") @DefaultValue("") String docId); + /** * Retrieves meta-data information for a source Document. * @@ -255,7 +353,10 @@ Response getResource(@PathParam("id") String idNoSlash, * could not be found with the given parameters.
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in * the server while performing this operation. + * + * Deprecated. Use {@link #getResourceMetaWithDocId} */ + @Deprecated @GET @Path(RESOURCE_SLUG_TEMPLATE + "/meta") // /r/{id}/meta @@ -264,6 +365,31 @@ Response getResource(@PathParam("id") String idNoSlash, Response getResourceMeta(@PathParam("id") String idNoSlash, @QueryParam("ext") Set extensions); + /** + * Retrieves meta-data information for a source Document. + * + * @param docId + * The document identifier. + * @param extensions + * The document extensions to retrieve with the document's + * meta-data (e.g. "gettext", "comment"). This parameter allows + * multiple values e.g. "ext=gettext&ext=comment". + * @return The following response status codes will be returned from this + * operation:
+ * OK(200) - If the Document's meta-data was found. The data will be + * contained in the response.
+ * NOT FOUND(404) - If a project, project iteration or document + * could not be found with the given parameters.
+ * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @GET + @Path(DOCID_RESOURCE_PATH + "/meta") + @TypeHint(ResourceMeta.class) + public Response getResourceMetaWithDocId( + @QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions); + /** * Modifies an existing source document's meta-data. * @@ -288,7 +414,9 @@ Response getResourceMeta(@PathParam("id") String idNoSlash, * permissions to perform this operation.
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in * the server while performing this operation. + * Deprecated. Use {@link #putResourceMetaWithDocId} */ + @Deprecated @PUT @Path(RESOURCE_SLUG_TEMPLATE + "/meta") // /r/{id}/meta @@ -297,4 +425,31 @@ Response putResourceMeta(@PathParam("id") String idNoSlash, ResourceMeta messageBody, @QueryParam("ext") Set extensions); + /** + * Modifies an existing source document's meta-data. + * + * @param docId + * The document identifier. + * @param messageBody + * The document's meta-data. + * @param extensions + * The document extensions to save with the document (e.g. + * "gettext", "comment"). This parameter allows multiple values + * e.g. "ext=gettext&ext=comment". + * @return The following response status codes will be returned from this + * operation:
+ * OK(200) - If the Document's meta-data was successfully modified.
+ * NOT FOUND(404) - If a document was not found using the given + * parameters.
+ * UNAUTHORIZED(401) - If the user does not have the proper + * permissions to perform this operation.
+ * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @PUT + @Path(DOCID_RESOURCE_PATH + "/meta") + public Response putResourceMetaWithDocId(ResourceMeta messageBody, + @QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions); + } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/StatisticsResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/StatisticsResource.java index dd11254e4e..3343bc336e 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/StatisticsResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/StatisticsResource.java @@ -29,8 +29,9 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.zanata.rest.dto.ProjectStatisticsMatrix; import org.zanata.rest.dto.stats.ContainerTranslationStatistics; import org.zanata.rest.dto.stats.contribution.ContributionStatistics; @@ -69,18 +70,18 @@ public interface StatisticsResource extends RestResource { * Locale statistics to be fetched. If this is empty, all locale * statistics will be returned. This parameter may be specified * multiple times if multiple locales are to be fetched. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response containing translation statistics for the - * specified parameters.
- * NOT FOUND(404) - If a project iteration could not be found for - * the given parameters.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @GET @Path("/proj/{projectSlug}/iter/{iterationSlug}") @TypeHint(ContainerTranslationStatistics.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Contains translation statistics" + + " for the specified parameters"), + @ResponseCode(code = 404, condition = "A project iteration could " + + "not be found for the given parameters"), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) public ContainerTranslationStatistics getStatistics( @@ -106,18 +107,18 @@ public interface StatisticsResource extends RestResource { * Locale statistics to be fetched. If this is empty, all locale * statistics will be returned. This parameter may be specified * multiple times if multiple locales are to be fetched. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response containing translation statistics for the - * specified parameters.
- * NOT FOUND(404) - If a document could not be found for the given - * parameters.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @GET @Path("/proj/{projectSlug}/iter/{iterationSlug}/doc/{docId:.*}") @TypeHint(ContainerTranslationStatistics.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Contains translation statistics" + + " for the specified parameters"), + @ResponseCode(code = 404, condition = "A document could " + + "not be found for the given parameters"), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) public ContainerTranslationStatistics getStatistics( @@ -139,23 +140,21 @@ public interface StatisticsResource extends RestResource { * username of contributor * @param dateRange * date range from..to (yyyy-mm-dd..yyyy-mm-dd) - * * @param includeAutomatedEntry * whether to include automatic entries of translation into statistic - * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response containing contribution statistics for the - * specified parameters.
- * BAD REQUEST(400) - If dateRange param is invalid.
- * NOT FOUND(404) - If a version or user could not be found.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @GET @Path("/project/{projectSlug}/version/{versionSlug}/contributor/{username}/{dateRange}") @TypeHint(ContributionStatistics.class) @Produces({ MediaType.APPLICATION_JSON }) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Contains contribution statistics" + + " for the specified parameters"), + @ResponseCode(code = 404, condition = "A project version could " + + "not be found for the given parameters"), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) public ContributionStatistics getContributionStatistics( @PathParam("projectSlug") String projectSlug, diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslatedDocResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslatedDocResource.java index af48c7d7c4..a30ea6dbf1 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslatedDocResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslatedDocResource.java @@ -21,6 +21,7 @@ package org.zanata.rest.service; +import static org.zanata.rest.service.SourceDocResource.DOCID_RESOURCE_PATH; import static org.zanata.rest.service.SourceDocResource.RESOURCE_SLUG_TEMPLATE; import java.util.Set; @@ -39,6 +40,8 @@ import javax.ws.rs.core.Response; import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import org.jboss.resteasy.util.HttpHeaderNames; import org.zanata.common.LocaleId; import org.zanata.rest.dto.resource.TranslationsResource; @@ -60,10 +63,13 @@ @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @ResourceLabel("Translated Documents") +@StatusCodes({ + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") +}) public interface TranslatedDocResource extends RestResource { @SuppressWarnings("deprecation") - public static final String SERVICE_PATH = - ProjectIterationResource.SERVICE_PATH + "/r"; + public static final String SERVICE_PATH = ProjectIterationResource.SERVICE_PATH; /** * Retrieves a set of translations for a given locale. @@ -85,29 +91,66 @@ public interface TranslatedDocResource extends RestResource { * An Entity tag identifier. Based on this identifier (if * provided), the server will decide if it needs to send a * response to the client or not (See return section). - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Successfully retrieved translations. The data will be - * contained in the response.
- * NOT FOUND(404) - If a project, project iteration or document - * could not be found with the given parameters. Also if no - * translations are found for the given document and locale.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation.
- * NOT_MODIFIED(304) - If the provided ETag matches the server's - * stored ETag, it will reply with this code, indicating that the - * last received response is still valid and should be reused. + * Deprecated. Use {@link #getTranslationsWithDocId} */ @GET @Path(RESOURCE_SLUG_TEMPLATE + "/translations/{locale}") // /r/{id}/translations/{locale} - @TypeHint(TranslationsResource.class) - public - Response getTranslations(@PathParam("id") String idNoSlash, - @PathParam("locale") LocaleId locale, - @QueryParam("ext") Set extensions, - @QueryParam("skeletons") boolean createSkeletons, - @HeaderParam(HttpHeaderNames.IF_NONE_MATCH) String eTag); + @TypeHint(TranslationsResource.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Successfully retrieved translations. " + + "The translation data will be contained in the response."), + @ResponseCode(code = 404, condition = "The project, version, or document could" + + " not be found with the given parameters. Also, if no translations are" + + " found for the given document and locale."), + @ResponseCode(code = 304, condition = "If the provided ETag matches the server's" + + " stored ETag, indicating that the last received response is still valid" + + " and should be reused."), + }) + @Deprecated + public + Response getTranslations(@PathParam("id") String idNoSlash, + @PathParam("locale") LocaleId locale, + @QueryParam("ext") Set extensions, + @QueryParam("skeletons") boolean skeletons, + @HeaderParam(HttpHeaderNames.IF_NONE_MATCH) String eTag); + + /** + * Retrieves a set of translations for a given locale. + * + * @param docId + * The document identifier. + * @param locale + * The locale for which to get translations. + * @param extensions + * The translation extensions to retrieve (e.g. "comment"). This + * parameter allows multiple values. + * @param createSkeletons + * Indicates whether to generate untranslated entries or not. + * @param eTag + * An Entity tag identifier. Based on this identifier (if + * provided), the server will decide if it needs to send a + * response to the client or not (See return section). + */ + @GET + @Path(DOCID_RESOURCE_PATH + "/translations/{locale}") + @TypeHint(TranslationsResource.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Successfully retrieved translations. " + + "The translation data will be contained in the response."), + @ResponseCode(code = 404, condition = "The project, version, or document could" + + " not be found with the given parameters. Also, if no translations are" + + " found for the given document and locale."), + @ResponseCode(code = 304, condition = "If the provided ETag matches the server's" + + " stored ETag, indicating that the last received response is still valid" + + " and should be reused."), + }) + public Response getTranslationsWithDocId( + @PathParam("locale") LocaleId locale, + @QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions, + @QueryParam("skeletons") boolean createSkeletons, + @HeaderParam(HttpHeaderNames.IF_NONE_MATCH) String eTag); /** * Deletes a set of translations for a given locale. Also deletes any @@ -122,22 +165,47 @@ Response getTranslations(@PathParam("id") String idNoSlash, * (','). * @param locale * The locale for which to get translations. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Successfully deleted the translations.
- * NOT FOUND(404) - If a project, project iteration or document - * could not be found with the given parameters. UNAUTHORIZED(401) - - * If the user does not have the proper permissions to perform this - * operation.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * Deprecated. Use {@link #deleteTranslationsWithDocId} */ + @Deprecated @DELETE @Path(RESOURCE_SLUG_TEMPLATE + "/translations/{locale}") + @TypeHint(TypeHint.NO_CONTENT.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Successfully deleted the translations."), + @ResponseCode(code = 404, condition = "If a project, project iteration or document" + + " could not be found with the given parameters."), + @ResponseCode(code = 401, condition = "If the user does not have the proper" + + " permissions to perform this operation."), + }) // /r/{id}/translations/{locale} - public - Response deleteTranslations(@PathParam("id") String idNoSlash, - @PathParam("locale") LocaleId locale); + public + Response deleteTranslations(@PathParam("id") String idNoSlash, + @PathParam("locale") LocaleId locale); + + /** + * Deletes a set of translations for a given locale. Also deletes any + * extensions recorded for the translations in question. The system will + * keep history of the translations. + * + * @param docId + * The document identifier. + * @param locale + * The locale for which to get translations. + */ + @DELETE + @Path(DOCID_RESOURCE_PATH + "/translations/{locale}") + @TypeHint(TypeHint.NO_CONTENT.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "Successfully deleted the translations."), + @ResponseCode(code = 404, condition = "If a project, project iteration or document" + + " could not be found with the given parameters."), + @ResponseCode(code = 401, condition = "If the user does not have the proper" + + " permissions to perform this operation."), + }) + public Response deleteTranslationsWithDocId( + @PathParam("locale") LocaleId locale, + @QueryParam("docId") @DefaultValue("") String docId); /** * Updates the translations for a document and a locale. @@ -162,27 +230,68 @@ Response deleteTranslations(@PathParam("id") String idNoSlash, * Auto will check the history of your translations and will not * overwrite any translations for which it detects a previous * value is being pushed. - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Translations were successfully updated.
- * NOT FOUND(404) - If a project, project iteration or document - * could not be found with the given parameters.
- * UNAUTHORIZED(401) - If the user does not have the proper - * permissions to perform this operation.
- * BAD REQUEST(400) - If there are problems with the parameters - * passed. i.e. Merge type is not one of the accepted types. This - * response should have a content message indicating a reason.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. + * Deprecated. Use {@link #putTranslationsWithDocId} */ + @Deprecated @PUT @Path(RESOURCE_SLUG_TEMPLATE + "/translations/{locale}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "Translations were successfully updated."), + @ResponseCode(code = 404, condition = "If a project, project iteration or document" + + " could not be found with the given parameters."), + @ResponseCode(code = 401, condition = "If the user does not have the proper" + + " permissions to perform this operation."), + @ResponseCode(code = 400, condition = "If there are problems with the passed parameters." + + " e.g. Merge type is not one of the accepted types. This response should have a" + + " content message indicating a reason.", + type = @TypeHint(String.class)) + }) // /r/{id}/translations/{locale} - public - Response putTranslations(@PathParam("id") String idNoSlash, - @PathParam("locale") LocaleId locale, - TranslationsResource messageBody, - @QueryParam("ext") Set extensions, - @QueryParam("merge") @DefaultValue("auto") String merge); + public + Response putTranslations(@PathParam("id") String idNoSlash, + @PathParam("locale") LocaleId locale, + TranslationsResource messageBody, + @QueryParam("ext") Set extensions, + @QueryParam("merge") @DefaultValue("auto") String merge); + + /** + * Updates the translations for a document and a locale. + * + * @param docId + * The document identifier. + * @param locale + * The locale for which to get translations. + * @param messageBody + * The translations to modify. + * @param extensions + * The translation extension types to modify (e.g. "comment"). + * This parameter allows multiple values. + * @param merge + * Indicates how to deal with existing translations (valid + * options: 'auto', 'import'). Import will overwrite all current + * values with the values being pushed (even empty ones), while + * Auto will check the history of your translations and will not + * overwrite any translations for which it detects a previous + * value is being pushed. + */ + @PUT + @Path(DOCID_RESOURCE_PATH + "/translations/{locale}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "Translations were successfully updated."), + @ResponseCode(code = 404, condition = "If a project, project iteration or document" + + " could not be found with the given parameters."), + @ResponseCode(code = 401, condition = "If the user does not have the proper" + + " permissions to perform this operation."), + @ResponseCode(code = 400, condition = "If there are problems with the passed parameters." + + " e.g. Merge type is not one of the accepted types. This response should have a" + + " content message indicating a reason.", + type = @TypeHint(String.class)) + }) + public Response putTranslationsWithDocId( + @PathParam("locale") LocaleId locale, + TranslationsResource messageBody, + @QueryParam("docId") @DefaultValue("") String docId, + @QueryParam("ext") Set extensions, + @QueryParam("merge") @DefaultValue("auto") String merge); } diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslationMemoryResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslationMemoryResource.java index 40ab14a294..85e2cd9eca 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslationMemoryResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/TranslationMemoryResource.java @@ -57,12 +57,14 @@ public interface TranslationMemoryResource extends RestResource { @GET @Path("all") public Response getAllTranslationMemory( + @QueryParam("srcLocale") @Nullable LocaleId srcLocale, @QueryParam("locale") @Nullable LocaleId locale); @GET @Path("projects/{projectSlug}") public Response getProjectTranslationMemory( @PathParam("projectSlug") @Nonnull String projectSlug, + @QueryParam("srcLocale") @Nullable LocaleId srcLocale, @QueryParam("locale") @Nullable LocaleId locale); @GET @@ -70,6 +72,7 @@ public Response getProjectTranslationMemory( public Response getProjectIterationTranslationMemory( @PathParam("projectSlug") @Nonnull String projectSlug, @PathParam("iterationSlug") @Nonnull String iterationSlug, + @QueryParam("srcLocale") @Nullable LocaleId srcLocale, @QueryParam("locale") @Nullable LocaleId locale); @GET diff --git a/api/zanata-common-api/src/main/java/org/zanata/rest/service/VersionResource.java b/api/zanata-common-api/src/main/java/org/zanata/rest/service/VersionResource.java index ee8ca1d23e..166840a848 100644 --- a/api/zanata-common-api/src/main/java/org/zanata/rest/service/VersionResource.java +++ b/api/zanata-common-api/src/main/java/org/zanata/rest/service/VersionResource.java @@ -28,6 +28,8 @@ import javax.ws.rs.core.Response; import com.webcohesion.enunciate.metadata.rs.ResourceLabel; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.zanata.rest.MediaTypes; import org.zanata.rest.dto.VersionInfo; @@ -44,18 +46,18 @@ public interface VersionResource extends RestResource { /** * Retrieve Version information for the application. - * - * @return The following response status codes will be returned from this - * operation:
- * OK(200) - Response with the system's version information in the - * content.
- * INTERNAL SERVER ERROR(500) - If there is an unexpected error in - * the server while performing this operation. */ @GET @Produces({ MediaTypes.APPLICATION_ZANATA_VERSION_XML, MediaTypes.APPLICATION_ZANATA_VERSION_JSON }) @TypeHint(VersionInfo.class) + @StatusCodes({ + @ResponseCode(code = 200, + condition = "Response with the system's version information in " + + "the body"), + @ResponseCode(code = 500, + condition = "If there is an unexpected error in the server while performing this operation") + }) public Response get(); } diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000000..b3a3a3adf2 --- /dev/null +++ b/client/README.md @@ -0,0 +1,10 @@ +Holds most of Zanata's client code, including the Zanata Maven Plugin and Zanata CLI, but not the python client. +It also contains client-side bindings for interacting with a Zanata server's REST API. + +[![license](https://img.shields.io/github/license/zanata/zanata-platform.svg?maxAge=3600)](https://github.com/zanata/zanata-platform/blob/master/LICENSE) + +Latest version of zanata-cli: +[![Maven Central](https://img.shields.io/maven-central/v/org.zanata/zanata-cli.svg)](https://maven-badges.herokuapp.com/maven-central/org.zanata/zanata-cli) + +Latest version of zanata-maven-plugin: +[![Maven Central](https://img.shields.io/maven-central/v/org.zanata/zanata-maven-plugin.svg)](https://maven-badges.herokuapp.com/maven-central/org.zanata/zanata-maven-plugin) diff --git a/client/README.txt b/client/README.txt deleted file mode 100644 index 9722d79443..0000000000 --- a/client/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -Holds most of Zanata's client code, including the Zanata Maven Plugin and Zanata CLI, but not the python client. -It also contains REST stub for interacting with a Zanata server. diff --git a/client/stub-server/src/main/java/org/zanata/rest/service/MockAsynchronousProcessResource.java b/client/stub-server/src/main/java/org/zanata/rest/service/MockAsynchronousProcessResource.java index b59074a6c8..fa5d4a5290 100644 --- a/client/stub-server/src/main/java/org/zanata/rest/service/MockAsynchronousProcessResource.java +++ b/client/stub-server/src/main/java/org/zanata/rest/service/MockAsynchronousProcessResource.java @@ -40,7 +40,6 @@ public class MockAsynchronousProcessResource implements private static final long serialVersionUID = 8841332691985560066L; @Override - @SuppressWarnings("deprecation") // TODO: remove this test when parent method is removed public ProcessStatus startSourceDocCreation(String idNoSlash, String projectSlug, String iterationSlug, Resource resource, @@ -52,6 +51,14 @@ public ProcessStatus startSourceDocCreation(String idNoSlash, public ProcessStatus startSourceDocCreationOrUpdate(String idNoSlash, String projectSlug, String iterationSlug, Resource resource, Set extensions, @DefaultValue("true") boolean copytrans) { + return startSourceDocCreationOrUpdateWithDocId(projectSlug, + iterationSlug, resource, extensions, idNoSlash); + } + + @Override + public ProcessStatus startSourceDocCreationOrUpdateWithDocId( + String projectSlug, String iterationSlug, Resource resource, + Set extensions, String docId) { ProcessStatus processStatus = new ProcessStatus(); processStatus.setStatusCode(ProcessStatus.ProcessStatusCode.Running); processStatus.setPercentageComplete(50); @@ -64,6 +71,17 @@ public ProcessStatus startTranslatedDocCreationOrUpdate(String idNoSlash, String projectSlug, String iterationSlug, LocaleId locale, TranslationsResource translatedDoc, Set extensions, String merge, @DefaultValue("false") boolean myTrans) { + return startTranslatedDocCreationOrUpdateWithDocId(projectSlug, + iterationSlug, locale, translatedDoc, idNoSlash, extensions, + merge, myTrans); + } + + @Override + public ProcessStatus startTranslatedDocCreationOrUpdateWithDocId( + String projectSlug, String iterationSlug, LocaleId locale, + TranslationsResource translatedDoc, String docId, + Set extensions, + String merge, boolean assignCreditToUploader) { ProcessStatus processStatus = new ProcessStatus(); processStatus.setStatusCode(ProcessStatus.ProcessStatusCode.Running); processStatus.setPercentageComplete(50); diff --git a/client/stub-server/src/main/java/org/zanata/rest/service/MockSourceDocResource.java b/client/stub-server/src/main/java/org/zanata/rest/service/MockSourceDocResource.java index 9da43d3a3a..071cd19c62 100644 --- a/client/stub-server/src/main/java/org/zanata/rest/service/MockSourceDocResource.java +++ b/client/stub-server/src/main/java/org/zanata/rest/service/MockSourceDocResource.java @@ -66,30 +66,58 @@ public Response post(Resource resource, Set extensions, @Override public Response getResource(String idNoSlash, Set extensions) { + return getResourceWithDocId(idNoSlash, extensions); + } + + @Override + public Response getResourceWithDocId(String docId, Set extensions) { MockResourceUtil.validateExtensions(extensions); - return Response.ok(new Resource(idNoSlash)).build(); + return Response.ok(new Resource(docId)).build(); } @Override public Response putResource(String idNoSlash, Resource resource, Set extensions, @DefaultValue("true") boolean copyTrans) { + return putResourceWithDocId(resource, idNoSlash, extensions, copyTrans); + } + + @Override + public Response putResourceWithDocId(Resource resource, String docId, + Set extensions, boolean copytrans) { MockResourceUtil.validateExtensions(extensions); return Response.ok(resource.getName()).build(); } @Override public Response deleteResource(String idNoSlash) { + return deleteResourceWithDocId(idNoSlash); + } + + @Override + public Response deleteResourceWithDocId(String docId) { return Response.ok().build(); } @Override public Response getResourceMeta(String idNoSlash, Set extensions) { + return getResourceMetaWithDocId(idNoSlash, extensions); + } + + @Override + public Response getResourceMetaWithDocId(String docId, + Set extensions) { return MockResourceUtil.notUsedByClient(); } @Override public Response putResourceMeta(String idNoSlash, ResourceMeta resourceMeta, Set extensions) { + return putResourceMetaWithDocId(resourceMeta, idNoSlash, extensions); + } + + @Override + public Response putResourceMetaWithDocId(ResourceMeta messageBody, + String docId, Set extensions) { return MockResourceUtil.notUsedByClient(); } } diff --git a/client/stub-server/src/main/java/org/zanata/rest/service/MockTranslatedDocResource.java b/client/stub-server/src/main/java/org/zanata/rest/service/MockTranslatedDocResource.java index f0e18ce148..07c11594b0 100644 --- a/client/stub-server/src/main/java/org/zanata/rest/service/MockTranslatedDocResource.java +++ b/client/stub-server/src/main/java/org/zanata/rest/service/MockTranslatedDocResource.java @@ -44,14 +44,26 @@ public class MockTranslatedDocResource implements TranslatedDocResource { public Response getTranslations(String idNoSlash, LocaleId locale, Set extensions, boolean createSkeletons, @HeaderParam("If-None-Match") String eTag) { + return getTranslationsWithDocId(locale, idNoSlash, extensions, + createSkeletons, eTag); + } + + @Override + public Response getTranslationsWithDocId(LocaleId locale, String docId, + Set extensions, boolean createSkeletons, String eTag) { MockResourceUtil.validateExtensions(extensions); TranslationsResource transResource = new TranslationsResource(); - transResource.getTextFlowTargets().add(new TextFlowTarget(idNoSlash)); + transResource.getTextFlowTargets().add(new TextFlowTarget(docId)); return Response.ok(transResource).build(); } @Override public Response deleteTranslations(String idNoSlash, LocaleId locale) { + return deleteTranslationsWithDocId(locale, idNoSlash); + } + + @Override + public Response deleteTranslationsWithDocId(LocaleId locale, String docId) { return MockResourceUtil.notUsedByClient(); } @@ -59,6 +71,14 @@ public Response deleteTranslations(String idNoSlash, LocaleId locale) { public Response putTranslations(String idNoSlash, LocaleId locale, TranslationsResource messageBody, Set extensions, @DefaultValue("auto") String merge) { + return putTranslationsWithDocId(locale, messageBody, idNoSlash, + extensions, merge); + } + + @Override + public Response putTranslationsWithDocId(LocaleId locale, + TranslationsResource messageBody, String docId, Set extensions, + String merge) { // used by PublicanPush only MockResourceUtil.validateExtensions(extensions); return Response.ok().build(); diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/PullCommand.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/PullCommand.java index ca35680c71..c243778be5 100644 --- a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/PullCommand.java +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/PullCommand.java @@ -10,7 +10,6 @@ import java.util.SortedSet; import java.util.TreeSet; -import javax.ws.rs.NotFoundException; import javax.ws.rs.client.ResponseProcessingException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; @@ -26,7 +25,6 @@ import org.zanata.client.exceptions.ConfigException; import org.zanata.common.LocaleId; import org.zanata.common.io.FileDetails; -import org.zanata.rest.RestUtil; import org.zanata.rest.client.RestClientFactory; import org.zanata.rest.dto.resource.Resource; import org.zanata.rest.dto.resource.TranslationsResource; @@ -225,12 +223,9 @@ && getOpts().getPullType() == PushPullType.Source) { try { Resource doc = null; String localDocName = unqualifiedDocName(qualifiedDocName); - // TODO follow a Link instead of generating the URI - String docUri = - RestUtil.convertToDocumentURIId(qualifiedDocName); boolean createSkeletons = getOpts().getCreateSkeletons(); if (strat.needsDocToWriteTrans() || pullSrc || createSkeletons) { - doc = sourceDocResourceClient.getResource(docUri, + doc = sourceDocResourceClient.getResource(qualifiedDocName, strat.getExtensions()); doc.setName(localDocName); } @@ -247,7 +242,7 @@ && getOpts().getPullType() == PushPullType.Source) { locMapping); if (shouldPullThisLocale(optionalStats, localDocName, locale)) { - pullDocForLocale(strat, doc, localDocName, docUri, + pullDocForLocale(strat, doc, localDocName, qualifiedDocName, createSkeletons, locMapping, transFile); } else { skippedLocales.add(locale); @@ -290,7 +285,7 @@ && getOpts().getPullType() == PushPullType.Source) { @VisibleForTesting protected void pullDocForLocale(PullStrategy strat, Resource doc, - String localDocName, String docUri, boolean createSkeletons, + String localDocName, String docId, boolean createSkeletons, LocaleMapping locMapping, File transFile) throws IOException { LocaleId locale = new LocaleId(locMapping.getLocale()); @@ -313,7 +308,7 @@ protected void pullDocForLocale(PullStrategy strat, Resource doc, Response transResponse; try { - transResponse = transDocResourceClient.getTranslations(docUri, + transResponse = transDocResourceClient.getTranslations(docId, locale, strat.getExtensions(), createSkeletons, eTag); } catch (ResponseProcessingException e) { @@ -352,7 +347,7 @@ protected void pullDocForLocale(PullStrategy strat, Resource doc, .getLocalFileMD5())) { transResponse = transDocResourceClient.getTranslations( - docUri, locale, + docId, locale, strat.getExtensions(), createSkeletons, null); // rewrite the target document diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/RawPullCommand.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/RawPullCommand.java index 91dff421fb..d3cb8652ff 100644 --- a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/RawPullCommand.java +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/pull/RawPullCommand.java @@ -232,9 +232,9 @@ public void run() throws IOException { private void pullDocForLocale(RawPullStrategy strat, String qualifiedDocName, String localDocName, String fileExtension, LocaleMapping locMapping, LocaleId locale) throws IOException { + Response response = null; try { - Response response = - fileResourceClient.downloadTranslationFile(getOpts() + response = fileResourceClient.downloadTranslationFile(getOpts() .getProj(), getOpts() .getProjectVersion(), locale.getId(), fileExtension, qualifiedDocName); @@ -264,6 +264,10 @@ private void pullDocForLocale(RawPullStrategy strat, } else { throw e; } + } finally { + if (response != null) { + response.close(); + } } } } diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java index fc0c3ec288..13079c862b 100644 --- a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/PushCommand.java @@ -15,7 +15,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.ws.rs.NotFoundException; import javax.ws.rs.client.ResponseProcessingException; import org.apache.commons.lang3.StringUtils; @@ -396,8 +395,6 @@ private void pushCurrentModule() throws IOException, RuntimeException { for (final String localDocName : docsToPush) { try { final String qualifiedDocName = qualifiedDocName(localDocName); - final String docUri = - RestUtil.convertToDocumentURIId(qualifiedDocName); final Resource srcDoc; if (strat.isTransOnly()) { srcDoc = null; @@ -407,7 +404,7 @@ private void pushCurrentModule() throws IOException, RuntimeException { debug(srcDoc); if (pushSource()) { - pushSrcDocToServer(docUri, srcDoc, extensions); + pushSrcDocToServer(qualifiedDocName, srcDoc, extensions); } } @@ -427,7 +424,7 @@ public void visit(LocaleMapping locale, localDocName, locale); return; } - pushTargetDocToServer(docUri, locale, + pushTargetDocToServer(qualifiedDocName, locale, qualifiedDocName, targetDoc, extensions); } @@ -553,20 +550,17 @@ private void deleteSourceDocsFromServer(List qualifiedDocNames) { } } - private void pushSrcDocToServer(final String docUri, final Resource srcDoc, + private void pushSrcDocToServer(final String docId, final Resource srcDoc, final StringSet extensions) { if (!getOpts().isDryRun()) { log.info("pushing source doc [name={} size={}] to server", srcDoc.getName(), srcDoc.getTextFlows().size()); ConsoleUtils.startProgressFeedback(); - // NB: Copy trans is set to false as using copy trans in this manner - // is deprecated. - // see PushCommand.copyTransForDocument ProcessStatus status = - asyncProcessClient.startSourceDocCreationOrUpdate(docUri, + asyncProcessClient.startSourceDocCreationOrUpdateWithDocId( getOpts().getProj(), getOpts().getProjectVersion(), - srcDoc, extensions, false); + srcDoc, extensions, docId); boolean waitForCompletion = true; @@ -594,13 +588,17 @@ private void pushSrcDocToServer(final String docUri, final Resource srcDoc, // try to submit the process again status = asyncProcessClient - .startSourceDocCreationOrUpdate(docUri, + .startSourceDocCreationOrUpdateWithDocId( getOpts().getProj(), getOpts() .getProjectVersion(), - srcDoc, extensions, false); + srcDoc, extensions, docId); ConsoleUtils .setProgressFeedbackMessage("Waiting for other clients ..."); break; + case Cancelled: + waitForCompletion = false; + ConsoleUtils.setProgressFeedbackMessage("Process is cancelled"); + break; } // Wait before retrying @@ -668,7 +666,7 @@ public List splitIntoBatch(TranslationsResource doc, return targetDocList; } - private void pushTargetDocToServer(final String docUri, + private void pushTargetDocToServer(final String docId, LocaleMapping locale, final String localDocName, TranslationsResource targetDoc, final StringSet extensions) { if (!getOpts().isDryRun()) { @@ -681,12 +679,13 @@ private void pushTargetDocToServer(final String docUri, ConsoleUtils.startProgressFeedback(); ProcessStatus status = - asyncProcessClient.startTranslatedDocCreationOrUpdate( - docUri, getOpts().getProj(), getOpts() - .getProjectVersion(), - new LocaleId(locale.getLocale()), targetDoc, - extensions, getOpts().getMergeType(), - getOpts().isMyTrans()); + asyncProcessClient + .startTranslatedDocCreationOrUpdateWithDocId( + getOpts().getProj(), getOpts() + .getProjectVersion(), + new LocaleId(locale.getLocale()), targetDoc, + docId, extensions, getOpts().getMergeType(), + getOpts().isMyTrans()); boolean waitForCompletion = true; @@ -715,16 +714,21 @@ extensions, getOpts().getMergeType(), // try to submit the process again status = asyncProcessClient - .startTranslatedDocCreationOrUpdate(docUri, + .startTranslatedDocCreationOrUpdateWithDocId( getOpts().getProj(), getOpts() .getProjectVersion(), - new LocaleId(locale.getLocale()), - targetDoc, extensions, + new LocaleId( + locale.getLocale()), + targetDoc, docId, extensions, getOpts().getMergeType(), getOpts().isMyTrans()); ConsoleUtils .setProgressFeedbackMessage("Waiting for other clients ..."); break; + case Cancelled: + waitForCompletion = false; + ConsoleUtils.setProgressFeedbackMessage("Process is cancelled"); + break; } // Wait before retrying @@ -751,8 +755,7 @@ extensions, getOpts().getMergeType(), private void deleteSourceDocFromServer(String qualifiedDocName) { if (!getOpts().isDryRun()) { log.info("deleting resource {} from server", qualifiedDocName); - String docUri = RestUtil.convertToDocumentURIId(qualifiedDocName); - sourceDocResourceClient.deleteResource(docUri); + sourceDocResourceClient.deleteResource(qualifiedDocName); } else { log.info( "deleting resource {} from server (skipped due to dry run)", diff --git a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/RawPushCommand.java b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/RawPushCommand.java index 569582993f..a0f9305910 100644 --- a/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/RawPushCommand.java +++ b/client/zanata-client-commands/src/main/java/org/zanata/client/commands/push/RawPushCommand.java @@ -714,7 +714,9 @@ public StreamChunker(File file, int chunkSize) @Override public void close() throws IOException { - fileStream.close(); + if (fileStream != null) { + fileStream.close(); + } } public int totalChunks() { diff --git a/client/zanata-client-commands/src/test/java/org/zanata/client/MockServerRule.java b/client/zanata-client-commands/src/test/java/org/zanata/client/MockServerRule.java index 31b5076e1e..65313a52d8 100644 --- a/client/zanata-client-commands/src/test/java/org/zanata/client/MockServerRule.java +++ b/client/zanata-client-commands/src/test/java/org/zanata/client/MockServerRule.java @@ -175,17 +175,17 @@ public PushCommand createPushCommand() { Collections.emptyList()); // this assumes async push is always success when( - asyncClient.startSourceDocCreationOrUpdate( - anyString(), - eq(pushOpts.getProj()), eq(pushOpts.getProjectVersion()), + asyncClient.startSourceDocCreationOrUpdateWithDocId( + eq(pushOpts.getProj()), + eq(pushOpts.getProjectVersion()), any(Resource.class), anySetOf(String.class), - eq(false))) + anyString())) .thenReturn(running); when( - asyncClient.startTranslatedDocCreationOrUpdate( - docIdCaptor.capture(), eq(pushOpts.getProj()), + asyncClient.startTranslatedDocCreationOrUpdateWithDocId( + eq(pushOpts.getProj()), eq(pushOpts.getProjectVersion()), localeIdCaptor.capture(), - transResourceCaptor.capture(), + transResourceCaptor.capture(), docIdCaptor.capture(), extensionCaptor.capture(), eq(pushOpts.getMergeType()), eq(pushOpts.isMyTrans()))) .thenReturn(running); @@ -221,17 +221,18 @@ public ArgumentCaptor getTransResourceCaptor() { } public void verifyPushSource() { - verify(asyncClient).startSourceDocCreationOrUpdate( - docIdCaptor.capture(), eq(pushOpts.getProj()), + verify(asyncClient).startSourceDocCreationOrUpdateWithDocId( + eq(pushOpts.getProj()), eq(pushOpts.getProjectVersion()), resourceCaptor.capture(), - extensionCaptor.capture(), eq(false)); + extensionCaptor.capture(), docIdCaptor.capture()); } public void verifyPushTranslation() { - verify(asyncClient).startTranslatedDocCreationOrUpdate( - docIdCaptor.capture(), eq(pushOpts.getProj()), + verify(asyncClient).startTranslatedDocCreationOrUpdateWithDocId( + eq(pushOpts.getProj()), eq(pushOpts.getProjectVersion()), localeIdCaptor.capture(), - transResourceCaptor.capture(), extensionCaptor.capture(), + transResourceCaptor.capture(), docIdCaptor.capture(), + extensionCaptor.capture(), eq(pushOpts.getMergeType()), eq(pushOpts.isMyTrans())); } diff --git a/client/zanata-client-commands/src/test/java/org/zanata/client/commands/push/PushCommandTest.java b/client/zanata-client-commands/src/test/java/org/zanata/client/commands/push/PushCommandTest.java index a71a9e66a9..5a361f34d4 100644 --- a/client/zanata-client-commands/src/test/java/org/zanata/client/commands/push/PushCommandTest.java +++ b/client/zanata-client-commands/src/test/java/org/zanata/client/commands/push/PushCommandTest.java @@ -179,14 +179,14 @@ private void push(boolean pushTrans, boolean mapLocale) throws Exception { mockStatus.setStatusCode(ProcessStatus.ProcessStatusCode.Finished); mockStatus.setMessages(new ArrayList()); when( - asyncProcessClient.startSourceDocCreationOrUpdate( - eq("RPM"), anyString(), anyString(), - any(Resource.class), eq(extensionSet), eq(false))) + asyncProcessClient.startSourceDocCreationOrUpdateWithDocId( + anyString(), anyString(), any(Resource.class), + eq(extensionSet), eq("RPM"))) .thenReturn(mockStatus); when( - asyncProcessClient.startSourceDocCreationOrUpdate( - eq("sub,RPM"), anyString(), anyString(), - any(Resource.class), eq(extensionSet), eq(false))) + asyncProcessClient.startSourceDocCreationOrUpdateWithDocId( + anyString(), anyString(), any(Resource.class), + eq(extensionSet), eq("sub/RPM"))) .thenReturn(mockStatus); when(asyncProcessClient.getProcessStatus(anyString())) .thenReturn(mockStatus); @@ -208,10 +208,11 @@ private void push(boolean pushTrans, boolean mapLocale) throws Exception { } when( asyncProcessClient - .startTranslatedDocCreationOrUpdate(eq("RPM"), + .startTranslatedDocCreationOrUpdateWithDocId( anyString(), anyString(), eq(expectedLocale), any(TranslationsResource.class), + eq("RPM"), eq(extensionSet), eq("auto"), eq(false))). thenReturn(mockStatus); // when(mockTranslationResources.putTranslations(eq("RPM"), diff --git a/client/zanata-rest-client/src/main/java/org/zanata/rest/client/AsyncProcessClient.java b/client/zanata-rest-client/src/main/java/org/zanata/rest/client/AsyncProcessClient.java index a5c2d93e22..8d80f9b104 100644 --- a/client/zanata-rest-client/src/main/java/org/zanata/rest/client/AsyncProcessClient.java +++ b/client/zanata-rest-client/src/main/java/org/zanata/rest/client/AsyncProcessClient.java @@ -32,6 +32,7 @@ import javax.ws.rs.core.Response; import org.zanata.common.LocaleId; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.ProcessStatus; import org.zanata.rest.dto.resource.Resource; import org.zanata.rest.dto.resource.TranslationsResource; @@ -53,8 +54,6 @@ public class AsyncProcessClient implements AsynchronousProcessResource { } @Override - @SuppressWarnings("deprecation") - // TODO: remove this test when parent method is removed public ProcessStatus startSourceDocCreation(String idNoSlash, String projectSlug, String iterationSlug, Resource resource, Set extensions, @DefaultValue("true") boolean copytrans) { @@ -63,6 +62,7 @@ public ProcessStatus startSourceDocCreation(String idNoSlash, } @Override + @Deprecated public ProcessStatus startSourceDocCreationOrUpdate(String idNoSlash, String projectSlug, String iterationSlug, Resource resource, Set extensions, @DefaultValue("true") boolean copytrans) { @@ -82,6 +82,34 @@ public ProcessStatus startSourceDocCreationOrUpdate(String idNoSlash, } @Override + public ProcessStatus startSourceDocCreationOrUpdateWithDocId( + String projectSlug, String iterationSlug, Resource resource, + Set extensions, String docId) { + Client client = factory.getClient(); + WebTarget webResource = client.target(baseUri) + .path(AsynchronousProcessResource.SERVICE_PATH) + .path("projects").path("p").path(projectSlug) + .path("iterations").path("i").path(iterationSlug) + .path("resource"); + Response response = webResource + .queryParam("docId", docId) + .queryParam("ext", extensions.toArray()) + .request(MediaType.APPLICATION_XML_TYPE) + .put(Entity.xml(resource)); + if (RestUtil.isNotFound(response)) { + response.close(); + // fallback to old endpoint + String idNoSlash = RestUtil.convertToDocumentURIId(docId); + return startSourceDocCreationOrUpdate(idNoSlash, projectSlug, + iterationSlug, resource, extensions, false); + } else { + response.bufferEntity(); + return response.readEntity(ProcessStatus.class); + } + } + + @Override + @Deprecated public ProcessStatus startTranslatedDocCreationOrUpdate(String idNoSlash, String projectSlug, String iterationSlug, LocaleId locale, TranslationsResource translatedDoc, Set extensions, @@ -104,6 +132,39 @@ public ProcessStatus startTranslatedDocCreationOrUpdate(String idNoSlash, return response.readEntity(ProcessStatus.class); } + @Override + public ProcessStatus startTranslatedDocCreationOrUpdateWithDocId( + String projectSlug, String iterationSlug, LocaleId locale, + TranslationsResource translatedDoc, String docId, + Set extensions, + String merge, boolean assignCreditToUploader) { + Client client = factory.getClient(); + WebTarget webResource = client.target(baseUri) + .path(AsynchronousProcessResource.SERVICE_PATH) + .path("projects").path("p").path(projectSlug) + .path("iterations").path("i").path(iterationSlug) + .path("resource") + .path("translations").path(locale.toString()); + Response response = webResource + .queryParam("docId", docId) + .queryParam("ext", extensions.toArray()) + .queryParam("merge", merge) + .queryParam("assignCreditToUploader", String.valueOf(assignCreditToUploader)) + .request(MediaType.APPLICATION_XML_TYPE) + .put(Entity.xml(translatedDoc)); + if (RestUtil.isNotFound(response)) { + // fallback to old endpoint + response.close(); + String idNoSlash = RestUtil.convertToDocumentURIId(docId); + return startTranslatedDocCreationOrUpdate(idNoSlash, projectSlug, + iterationSlug, locale, translatedDoc, extensions, merge, + assignCreditToUploader); + } else { + response.bufferEntity(); + return response.readEntity(ProcessStatus.class); + } + } + @Override public ProcessStatus getProcessStatus(String processId) { return factory.getClient().target(baseUri) diff --git a/client/zanata-rest-client/src/main/java/org/zanata/rest/client/SourceDocResourceClient.java b/client/zanata-rest-client/src/main/java/org/zanata/rest/client/SourceDocResourceClient.java index e41a2cb5e3..e9bb04cd6c 100644 --- a/client/zanata-rest-client/src/main/java/org/zanata/rest/client/SourceDocResourceClient.java +++ b/client/zanata-rest-client/src/main/java/org/zanata/rest/client/SourceDocResourceClient.java @@ -32,6 +32,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.resource.Resource; import org.zanata.rest.dto.resource.ResourceMeta; @@ -61,7 +62,7 @@ public class SourceDocResourceClient { public List getResourceMeta(Set extensions) { Client client = factory.getClient(); - WebTarget webResource = getBaseServiceResource(client); + WebTarget webResource = getBaseServiceResource(client).path("r"); if (extensions != null) { webResource.queryParam("ext", extensions.toArray()); } @@ -74,38 +75,71 @@ private WebTarget getBaseServiceResource(Client client) { .path("projects").path("p") .path(project) .path("iterations").path("i") - .path(projectVersion) - .path("r"); + .path(projectVersion); } - public Resource getResource(String idNoSlash, Set extensions) { + public Resource getResource(String docId, Set extensions) { Client client = factory.getClient(); WebTarget webResource = getBaseServiceResource(client) - .path(idNoSlash) + .path("resource") + .queryParam("docId", docId) .queryParam("ext", extensions.toArray()); - return webResource.request(MediaType.APPLICATION_XML_TYPE) - .get(Resource.class); + Response response = webResource.request(MediaType.APPLICATION_XML_TYPE) + .get(); + if (RestUtil.isNotFound(response)) { + // fallback to old endpoint + response.close(); + String idNoSlash = RestUtil.convertToDocumentURIId(docId); + webResource = + getBaseServiceResource(client) + .path("r") + .path(idNoSlash) + .queryParam("ext", extensions.toArray()); + return webResource.request(MediaType.APPLICATION_XML_TYPE) + .get(Resource.class); + } + return response.readEntity(Resource.class); } - public String putResource(String idNoSlash, Resource resource, + public String putResource(String docId, Resource resource, Set extensions, boolean copyTrans) { Client client = factory.getClient(); - WebTarget webResource = getBaseServiceResource(client) - .path(idNoSlash) + WebTarget webResource = getBaseServiceResource(client).path("resource") + .queryParam("docId", docId) .queryParam("ext", extensions.toArray()) .queryParam("copyTrans", String.valueOf(copyTrans)); Response response = webResource.request(MediaType.APPLICATION_XML_TYPE) .put(Entity.entity(resource, MediaType.APPLICATION_XML_TYPE)); + if (RestUtil.isNotFound(response)) { + // fallback to old endpoint + response.close(); + String idNoSlash = RestUtil.convertToDocumentURIId(docId); + webResource = getBaseServiceResource(client) + .path("r") + .path(idNoSlash) + .queryParam("ext", extensions.toArray()) + .queryParam("copyTrans", String.valueOf(copyTrans)); + response = webResource.request(MediaType.APPLICATION_XML_TYPE) + .put(Entity.entity(resource, MediaType.APPLICATION_XML_TYPE)); + } response.bufferEntity(); return response.readEntity(String.class); } - public String deleteResource(String idNoSlash) { + public String deleteResource(String docId) { Client client = factory.getClient(); WebTarget webResource = getBaseServiceResource(client); - return webResource.path(idNoSlash).request().delete(String.class); + Response response = + webResource.path("resource").queryParam("docId", docId).request() + .delete(); + if (RestUtil.isNotFound(response)) { + response.close(); + String idNoSlash = RestUtil.convertToDocumentURIId(docId); + return webResource.path("r").path(idNoSlash).request() + .delete(String.class); + } + return response.readEntity(String.class); } - } diff --git a/client/zanata-rest-client/src/main/java/org/zanata/rest/client/TransDocResourceClient.java b/client/zanata-rest-client/src/main/java/org/zanata/rest/client/TransDocResourceClient.java index cc10fc86e2..d59a390ee5 100644 --- a/client/zanata-rest-client/src/main/java/org/zanata/rest/client/TransDocResourceClient.java +++ b/client/zanata-rest-client/src/main/java/org/zanata/rest/client/TransDocResourceClient.java @@ -30,6 +30,7 @@ import javax.ws.rs.core.Response; import org.zanata.common.LocaleId; +import org.zanata.rest.RestUtil; /** * This "implements" caller methods to endpoints in TranslatedDocResource. @@ -51,21 +52,34 @@ public class TransDocResourceClient { baseUri = factory.getBaseUri(); } - public Response getTranslations( - String idNoSlash, - LocaleId locale, - Set extensions, - boolean createSkeletons, - String eTag) { + public Response getTranslations(String docId, LocaleId locale, + Set extensions, boolean createSkeletons, String eTag) { Client client = factory.getClient(); - return getBaseServiceResource(client) - .path(idNoSlash) - .path("translations").path(locale.getId()) + Response response = getBaseServiceResource(client) + .path("resource") + .path("translations") + .path(locale.getId()) + .queryParam("docId", docId) .queryParam("ext", extensions.toArray()) .queryParam("skeletons", String.valueOf(createSkeletons)) .request(MediaType.APPLICATION_XML_TYPE) .header(HttpHeaders.IF_NONE_MATCH, eTag) .get(); + if (RestUtil.isNotFound(response)) { + // fallback to old endpoint + response.close(); + String idNoSlash = RestUtil.convertToDocumentURIId(docId); + response = getBaseServiceResource(client) + .path("r") + .path(idNoSlash) + .path("translations").path(locale.getId()) + .queryParam("ext", extensions.toArray()) + .queryParam("skeletons", String.valueOf(createSkeletons)) + .request(MediaType.APPLICATION_XML_TYPE) + .header(HttpHeaders.IF_NONE_MATCH, eTag) + .get(); + } + return response; } private WebTarget getBaseServiceResource(Client client) { @@ -73,8 +87,7 @@ private WebTarget getBaseServiceResource(Client client) { .path("projects").path("p") .path(project) .path("iterations").path("i") - .path(projectVersion) - .path("r"); + .path(projectVersion); } } diff --git a/client/zanata-rest-client/src/test/java/org/zanata/rest/client/AsyncProcessClientTest.java b/client/zanata-rest-client/src/test/java/org/zanata/rest/client/AsyncProcessClientTest.java index bf9730be0c..5a39d4984d 100644 --- a/client/zanata-rest-client/src/test/java/org/zanata/rest/client/AsyncProcessClientTest.java +++ b/client/zanata-rest-client/src/test/java/org/zanata/rest/client/AsyncProcessClientTest.java @@ -50,11 +50,11 @@ public void setUp() throws Exception { @Test public void testStartSourceDocCreationOrUpdate() throws Exception { ProcessStatus processStatus = - client.startSourceDocCreationOrUpdate("message", + client.startSourceDocCreationOrUpdateWithDocId( "about-fedora", "master", new Resource("message"), Sets.newHashSet("gettext"), - false); + "message"); assertThat(processStatus.getStatusCode(), Matchers.equalTo( ProcessStatus.ProcessStatusCode.Running)); @@ -63,10 +63,11 @@ public void testStartSourceDocCreationOrUpdate() throws Exception { @Test public void testStartTranslatedDocCreationOrUpdate() throws Exception { ProcessStatus processStatus = - client.startTranslatedDocCreationOrUpdate("message", + client.startTranslatedDocCreationOrUpdateWithDocId( "about-fedora", "master", LocaleId.DE, - new TranslationsResource(), Sets.newHashSet("gettext"), + new TranslationsResource(), "message", + Sets.newHashSet("gettext"), "auto", false); assertThat(processStatus.getStatusCode(), Matchers.equalTo( diff --git a/client/zanata-rest-client/src/test/java/org/zanata/rest/client/SourceDocResourceClientTest.java b/client/zanata-rest-client/src/test/java/org/zanata/rest/client/SourceDocResourceClientTest.java index eb4e1619b3..b6a1cebe00 100644 --- a/client/zanata-rest-client/src/test/java/org/zanata/rest/client/SourceDocResourceClientTest.java +++ b/client/zanata-rest-client/src/test/java/org/zanata/rest/client/SourceDocResourceClientTest.java @@ -54,7 +54,6 @@ public void setUp() throws URISyntaxException { @Test public void testGetResourceMeta() { List resourceMeta = client.getResourceMeta(null); - assertThat(resourceMeta, Matchers.hasSize(2)); } @@ -62,7 +61,6 @@ public void testGetResourceMeta() { public void testGetResource() { Resource resource = client.getResource("test", Sets.newHashSet("gettext", "comment")); - assertThat(resource.getName(), Matchers.equalTo("test")); } diff --git a/common/zanata-common-util/src/main/java/org/zanata/CommonUtil.gwt.xml b/common/zanata-common-util/src/main/java/org/zanata/CommonUtil.gwt.xml index 118c114830..8305e55c63 100644 --- a/common/zanata-common-util/src/main/java/org/zanata/CommonUtil.gwt.xml +++ b/common/zanata-common-util/src/main/java/org/zanata/CommonUtil.gwt.xml @@ -3,6 +3,7 @@ + diff --git a/license-gpl.md b/license-gpl.md new file mode 100644 index 0000000000..00c115f306 --- /dev/null +++ b/license-gpl.md @@ -0,0 +1,361 @@ +### GNU GENERAL PUBLIC LICENSE + +Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +### Preamble + +The licenses for most software are designed to take away your freedom +to share and change it. By contrast, the GNU General Public License is +intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if +you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + +We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, +we want its recipients to know that what they have is not the +original, so that any problems introduced by others will not reflect +on the original authors' reputations. + +Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at +all. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +**0.** This License applies to any program or other work which +contains a notice placed by the copyright holder saying it may be +distributed under the terms of this General Public License. The +"Program", below, refers to any such program or work, and a "work +based on the Program" means either the Program or any derivative work +under copyright law: that is to say, a work containing the Program or +a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is +included without limitation in the term "modification".) Each licensee +is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the Program +(independent of having been made by running the Program). Whether that +is true depends on what the Program does. + +**1.** You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a +fee. + +**2.** You may modify your copy or copies of the Program or any +portion of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + +**a)** You must cause the modified files to carry prominent notices +stating that you changed the files and the date of any change. + + +**b)** You must cause any work that you distribute or publish, that in +whole or in part contains or is derived from the Program or any part +thereof, to be licensed as a whole at no charge to all third parties +under the terms of this License. + + +**c)** If the modified program normally reads commands interactively +when run, you must cause it, when started running for such interactive +use in the most ordinary way, to print or display an announcement +including an appropriate copyright notice and a notice that there is +no warranty (or else, saying that you provide a warranty) and that +users may redistribute the program under these conditions, and telling +the user how to view a copy of this License. (Exception: if the +Program itself is interactive but does not normally print such an +announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +**3.** You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + +**a)** Accompany it with the complete corresponding machine-readable +source code, which must be distributed under the terms of Sections 1 +and 2 above on a medium customarily used for software interchange; or, + + +**b)** Accompany it with a written offer, valid for at least three +years, to give any third party, for a charge no more than your cost of +physically performing source distribution, a complete machine-readable +copy of the corresponding source code, to be distributed under the +terms of Sections 1 and 2 above on a medium customarily used for +software interchange; or, + + +**c)** Accompany it with the information you received as to the offer +to distribute corresponding source code. (This alternative is allowed +only for noncommercial distribution and only if you received the +program in object code or executable form with such an offer, in +accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + +**4.** You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt otherwise +to copy, modify, sublicense or distribute the Program is void, and +will automatically terminate your rights under this License. However, +parties who have received copies, or rights, from you under this +License will not have their licenses terminated so long as such +parties remain in full compliance. + +**5.** You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +**6.** Each time you redistribute the Program (or any work based on +the Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + +**7.** If, as a consequence of a court judgment or allegation of +patent infringement or for any other reason (not limited to patent +issues), 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 distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, +then as a consequence you may not distribute the Program at all. For +example, if a patent license would not permit royalty-free +redistribution of the Program by all those who receive copies directly +or indirectly through you, then the only way you could satisfy both it +and this License would be to refrain entirely from distribution of the +Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +**8.** If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +**9.** The Free Software Foundation may publish revised and/or new +versions of the 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 a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Program does not specify a +version number of this License, you may choose any version ever +published by the Free Software Foundation. + +**10.** If you wish to incorporate parts of the Program into other +free programs whose distribution conditions are different, write to +the author to ask for permission. For software which is copyrighted by +the Free Software Foundation, write to the Free Software Foundation; +we sometimes make exceptions for this. Our decision will be guided by +the two goals of preserving the free status of all derivatives of our +free software and of promoting the sharing and reuse of software +generally. + +**NO WARRANTY** + +**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + +**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE 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. + +### 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 +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. + Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper +mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the +commands you use may be called something other than \`show w' and +\`show c'; they could even be mouse-clicks or menu items--whatever +suits your program. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the program, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright + interest in the program `Gnomovision' + (which makes passes at compilers) written + by James Hacker. + + signature of Ty Coon, 1 April 1989 + Ty Coon, President of Vice + +This 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](http://www.gnu.org/licenses/lgpl.html) instead of this +License. diff --git a/license.md b/license.md new file mode 100644 index 0000000000..b14d908353 --- /dev/null +++ b/license.md @@ -0,0 +1,503 @@ +### GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + [This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + +### Preamble + +The licenses for most software are designed to take away your freedom +to share and change it. By contrast, the GNU General Public Licenses +are intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + +When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + +To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there +is no warranty for the free library. Also, if the library is modified +by someone else and passed on, the recipients should know that what +they have is not the original version, so that the original author's +reputation will not be affected by problems that might be introduced +by others. + +Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + +When a program is linked with a library, whether statically or using a +shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + +We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + +For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + +Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + +### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +**0.** This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). Each +licensee is addressed as "you". + +A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does and +what the program that uses the Library does. + +**1.** You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a +fee. + +**2.** You may modify your copy or copies of the Library or any +portion of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + +- **a)** The modified work must itself be a software library. +- **b)** You must cause the files modified to carry prominent + notices stating that you changed the files and the date of + any change. +- **c)** You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. +- **d)** If a facility in the modified Library refers to a function + or a table of data to be supplied by an application program that + uses the facility, other than as an argument passed when the + facility is invoked, then you must make a good faith effort to + ensure that, in the event an application does not supply such + function or table, the facility still operates, and performs + whatever part of its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of + the application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +**3.** You may opt to apply the terms of the ordinary GNU General +Public License instead of this License to a given copy of the Library. +To do this, you must alter all the notices that refer to this License, +so that they refer to the ordinary GNU General Public License, version +2, instead of to this License. (If a newer version than version 2 of +the ordinary GNU General Public License has appeared, then you can +specify that version instead if you wish.) Do not make any other +change in these notices. + +Once this change is made in a given copy, it is irreversible for that +copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the +Library into a program that is not a library. + +**4.** You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from +a designated place, then offering equivalent access to copy the source +code from the same place satisfies the requirement to distribute the +source code, even though third parties are not compelled to copy the +source along with the object code. + +**5.** A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a work, +in isolation, is not a derivative work of the Library, and therefore +falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. Section +6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure +layouts and accessors, and small macros and small inline functions +(ten lines or less in length), then the use of the object file is +unrestricted, regardless of whether it is legally a derivative work. +(Executables containing this object code plus portions of the Library +will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + +**6.** As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a work +containing portions of the Library, and distribute that work under +terms of your choice, provided that the terms permit modification of +the work for the customer's own use and reverse engineering for +debugging such modifications. + +You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + +- **a)** Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood that + the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) +- **b)** Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (1) uses at run time + a copy of the library already present on the user's computer + system, rather than copying library functions into the executable, + and (2) will operate properly with a modified version of the + library, if the user installs one, as long as the modified version + is interface-compatible with the version that the work was + made with. +- **c)** Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. +- **d)** If distribution of the work is made by offering access to + copy from a designated place, offer equivalent access to copy the + above specified materials from the same place. +- **e)** Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + +**7.** You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + +- **a)** Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other + library facilities. This must be distributed under the terms of + the Sections above. +- **b)** Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + +**8.** You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + +**9.** You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +**10.** Each time you redistribute the Library (or any work based on +the Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + +**11.** If, as a consequence of a court judgment or allegation of +patent infringement or for any other reason (not limited to patent +issues), 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 distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, +then as a consequence you may not distribute the Library at all. For +example, if a patent license would not permit royalty-free +redistribution of the Library by all those who receive copies directly +or indirectly through you, then the only way you could satisfy both it +and this License would be to refrain entirely from distribution of the +Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +**12.** If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + +**13.** The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + +**14.** If you wish to incorporate parts of the Library into other +free programs whose distribution conditions are incompatible with +these, write to the author to ask for permission. For software which +is copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +**NO WARRANTY** + +**15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +**16.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +### END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It +is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper +mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in + the library `Frob' (a library for tweaking knobs) written + by James Random Hacker. + + signature of Ty Coon, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/server/docker/README.md b/server/docker/README.md index b3e4adeb4c..b52a0c0040 100644 --- a/server/docker/README.md +++ b/server/docker/README.md @@ -85,7 +85,9 @@ $ ./rundb.sh This script will start a docker container with the database. You can inspect the script file to learn the exact docker command it's running. -The container will map the mysql data directory to `$HOME/docker-volumes/zanata-mariadb`. This can be changed from the script file. +The container by default will map the mysql data directory to `$HOME/docker-volumes/zanata-mariadb`. This can be changed from the script file. + +If you give the script ```-e`` option (stands for ephemeral), it will not use any volume mapping. This means any data you save in Zanata using this mode will be lost once the container is stopped. It will also remove itself once stopped (e.g. no need to call docker rm zanatadb). This is useful for testing a fresh copy of Zanata instance. The database can be accessed via tcp via the `mysql` command or by using any database administration tool. You need to get the actual mapped port on the host by typing `docker ps`. After you have the port, you can connect locally to the database. The following is an example to accomplish this using a locally installed mysql client: diff --git a/server/docker/rundb.sh b/server/docker/rundb.sh index eca53b7768..080a0a2129 100755 --- a/server/docker/rundb.sh +++ b/server/docker/rundb.sh @@ -16,7 +16,10 @@ VOLUME_DIR=$HOME/docker-volumes/zanata-mariadb mkdir -p $VOLUME_DIR -while getopts ":n:h" opt; do +CONTAINER_NAME=zanatadb +VOLUME_OPT="-v $VOLUME_DIR:/var/lib/mysql:Z" +EPHEMERAL=false +while getopts ":n:eh" opt; do case ${opt} in n) echo "===== set docker network to $OPTARG =====" @@ -28,6 +31,17 @@ while getopts ":n:h" opt; do echo "-h : display help" exit ;; + e) + echo "===== starting an ephemeral database container ====" + if [ ! -z $(docker ps --all --quiet --filter name=$CONTAINER_NAME) ]; then + echo "removing $CONTAINER_NAME container" + docker rm -f $CONTAINER_NAME + fi + # instead of giving the container volume mapping, + # we tell it to remove itself once stopped. + VOLUME_OPT=" --rm " + EPHEMERAL=true + ;; \?) echo "Invalid option: -${OPTARG}. Use -h for help" >&2 exit 1 @@ -37,13 +51,22 @@ done ensure_docker_network -docker run --name zanatadb \ +docker run --name $CONTAINER_NAME \ -e MYSQL_USER=$DB_USERNAME -e MYSQL_PASSWORD=$DB_PASSWORD \ -e MYSQL_DATABASE=$DB_SCHEMA -e MYSQL_ROOT_PASSWORD=$DB_ROOT_PASSWORD \ -P --net=${DOCKER_NETWORK} \ - -v $VOLUME_DIR:/var/lib/mysql:Z \ + ${VOLUME_OPT} \ -d mariadb:10.1 \ --character-set-server=utf8 --collation-server=utf8_general_ci echo '' -echo 'Please use the command "docker logs zanatadb" to check that MariaDB starts correctly.' +echo "Please use the command 'docker logs $CONTAINER_NAME' to check that MariaDB starts correctly." + +if [ "$EPHEMERAL" == 'true' ] +then + echo "=====================================================================" + echo "Once MariaDB and Zanata are running, execute below commands to create an admin user" + echo "docker cp $DIR/conf/admin-user-setup.sql $CONTAINER_NAME:/tmp/" + echo "docker exec $CONTAINER_NAME /bin/sh -c 'mysql -u$DB_USERNAME -p$DB_PASSWORD $DB_SCHEMA test - - org.hamcrest - hamcrest-core - compile - - - org.hamcrest - hamcrest-library - compile - - org.jetbrains.kotlin kotlin-stdlib diff --git a/server/functional-test/src/main/java/org/zanata/page/AbstractPage.kt b/server/functional-test/src/main/java/org/zanata/page/AbstractPage.kt index d4611720f6..640ee3f6d2 100644 --- a/server/functional-test/src/main/java/org/zanata/page/AbstractPage.kt +++ b/server/functional-test/src/main/java/org/zanata/page/AbstractPage.kt @@ -482,6 +482,7 @@ abstract class AbstractPage(val driver: WebDriver) { } else { log.warn("Unable to focus page container") } + waitForPageSilence(); } /** diff --git a/server/functional-test/src/main/java/org/zanata/page/account/EnterNewPasswordPage.java b/server/functional-test/src/main/java/org/zanata/page/account/EnterNewPasswordPage.java new file mode 100644 index 0000000000..0e0f143497 --- /dev/null +++ b/server/functional-test/src/main/java/org/zanata/page/account/EnterNewPasswordPage.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors as indicated by the + * @author tags. See the copyright.txt file in the distribution for a full + * listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This software 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 Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this software; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF + * site: http://www.fsf.org. + */ +package org.zanata.page.account; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.zanata.page.BasePage; + +/** + * @author Damian Jansen + * djansen@redhat.com + */ +public class EnterNewPasswordPage extends BasePage { + private static final org.slf4j.Logger log = + org.slf4j.LoggerFactory.getLogger(EnterNewPasswordPage.class); + + public EnterNewPasswordPage(WebDriver driver) { + super(driver); + } + + private By newPassworField = + By.id("passwordResetActivationForm:passwordFieldContainer:input:password"); + private By confirmPassworField = + By.id("passwordResetActivationForm:confirmPasswordFieldContainer:input:passwordConfirm"); + private By resetPasswordButton = + By.id("passwordResetActivationForm:resetPasswordButton"); + + public EnterNewPasswordPage enterNewPassword(String password) { + log.info("Enter new password {}", password); + enterText(readyElement(newPassworField), password); + return new EnterNewPasswordPage(getDriver()); + } + + public EnterNewPasswordPage enterConfirmPassword(String password) { + log.info("Enter confirm password {}", password); + enterText(readyElement(confirmPassworField), password); + return new EnterNewPasswordPage(getDriver()); + } + + public SignInPage pressChangePasswordButton() { + log.info("Press Change Password button"); + clickElement(resetPasswordButton); + return new SignInPage(getDriver()); + } +} diff --git a/server/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java b/server/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java index cd3938348d..6319e0b135 100644 --- a/server/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java +++ b/server/functional-test/src/main/java/org/zanata/page/account/InactiveAccountPage.java @@ -44,15 +44,21 @@ public HomePage clickResendActivationEmail() { } public InactiveAccountPage enterNewEmail(String email) { - enterText( - readyElement( - By.id("inactiveAccountForm:email:input:emailInput")), - email); + log.info("Enter new email {}", email); + enterText(readyElement( + By.id("inactiveAccountForm:email:input:emailInput")), email); return new InactiveAccountPage(getDriver()); } public HomePage clickUpdateEmail() { + log.info("Click Update button"); clickElement(By.id("inactiveAccountForm:email:input:updateEmail")); return new HomePage(getDriver()); } + + public InactiveAccountPage updateEmailFailure() { + log.info("Click Update button, expecting failure"); + clickElement(By.id("inactiveAccountForm:email:input:updateEmail")); + return new InactiveAccountPage(getDriver()); + } } diff --git a/server/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java b/server/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java index e06b168252..c304e59994 100644 --- a/server/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java +++ b/server/functional-test/src/main/java/org/zanata/page/account/RegisterPage.java @@ -44,7 +44,7 @@ public class RegisterPage extends CorePage { public static final String MALFORMED_EMAIL_ERROR = "not a well-formed email address"; public static final String REQUIRED_FIELD_ERROR = "may not be empty"; - + public static final String EMAIL_TAKEN = "This email address is already taken."; public static final String PASSWORD_LENGTH_ERROR = "size must be between 6 and 1024"; diff --git a/server/functional-test/src/main/java/org/zanata/page/languages/LanguagePage.java b/server/functional-test/src/main/java/org/zanata/page/languages/LanguagePage.java index d0f00af1cd..455dafa91d 100644 --- a/server/functional-test/src/main/java/org/zanata/page/languages/LanguagePage.java +++ b/server/functional-test/src/main/java/org/zanata/page/languages/LanguagePage.java @@ -51,6 +51,9 @@ public class LanguagePage extends BasePage { private By addUserSearchButton = By.id("searchForm:searchBtn"); private By personTable = By.id("resultForm:searchResults"); private By addSelectedButton = By.id("addSelectedBtn"); + private By requestToJoinLanguage = By.xpath(".//button[contains(text(),'Request To Join')]"); + private By cancelRequest = By.xpath(".//span[contains(text(),'Cancel request')]"); + private By leaveLanguageTeam = By.xpath(".//span[contains(text(),'Leave Team')]"); public static final int IS_TRANSLATOR_COLUMN = 0; public static final int IS_REVIEWER_COLUMN = 1; public static final int IS_COORDINATOR_COLUMN = 2; @@ -234,4 +237,23 @@ public static enum TeamPermission { this.columnIndex = columnIndex; } } + + public RequestToJoinPopup requestToJoin() { + log.info("Click Request To Join"); + clickElement(requestToJoinLanguage); + return new RequestToJoinPopup(getDriver()); + } + + public LanguagePage leaveTeam() { + log.info("Click Leave Team"); + clickElement(leaveLanguageTeam); + return new LanguagePage(getDriver()); + } + + public LanguagePage cancelRequest() { + log.info("Click Cancel Request"); + clickElement(cancelRequest); + return new LanguagePage(getDriver()); + } + } diff --git a/server/functional-test/src/main/java/org/zanata/page/languages/RequestToJoinPopup.java b/server/functional-test/src/main/java/org/zanata/page/languages/RequestToJoinPopup.java new file mode 100644 index 0000000000..56148c0a92 --- /dev/null +++ b/server/functional-test/src/main/java/org/zanata/page/languages/RequestToJoinPopup.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors as indicated by the + * @author tags. See the copyright.txt file in the distribution for a full + * listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This software 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 Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this software; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF + * site: http://www.fsf.org. + */ + +package org.zanata.page.languages; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.zanata.page.BasePage; + +/** + * @author Sachin Pathare spathare@redhat.com + */ +public class RequestToJoinPopup extends BasePage { + private static final org.slf4j.Logger log = + org.slf4j.LoggerFactory.getLogger(RequestToJoinPopup.class); + private By messageField = By.id( + "joinLanguageForm:requestToJoinLanguageMessage:input:request-join-message"); + private By sendButton = By.id("request-join-language-send-button"); + private By cancelButton = By.id("request-join-language-cancel-button"); + + public RequestToJoinPopup(WebDriver driver) { + super(driver); + } + + public RequestToJoinPopup enterMessage(String message) { + log.info("Enter message {}", message); + enterText(readyElement(messageField), message); + return new RequestToJoinPopup(getDriver()); + } + + public LanguagePage clickSend() { + log.info("Click the Send Message button"); + clickElement(sendButton); + return new LanguagePage(getDriver()); + } + +} diff --git a/server/functional-test/src/test/java/org/zanata/feature/account/EmailValidationTest.java b/server/functional-test/src/test/java/org/zanata/feature/account/EmailValidationTest.java index 81a039c39d..3bb90079de 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/account/EmailValidationTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/account/EmailValidationTest.java @@ -53,25 +53,33 @@ public void before() { registerPage = new BasicWorkFlow().goToHome().goToRegistration(); } - @Trace( - summary = "The system will allow acceptable forms of an email address for registration") - @Test + @Trace(summary = "The system will allow acceptable forms of an email address for registration", + testPlanIds = 5681, testCaseIds = -1) + @Test(timeout = MAX_SHORT_TEST_DURATION) public void validEmailAcceptance() throws Exception { - registerPage = - // Shift to other field - registerPage.enterEmail("me@mydomain.com") - .enterName("Sam I Am"); + registerPage = registerPage.enterEmail("me@mydomain.com"); + registerPage.defocus(); + assertThat(registerPage.getErrors()) .as("Email validation errors are not shown").isEmpty(); } - @Trace( - summary = "The user must enter a valid email address to register with Zanata") - @Test + @Trace(summary = "The user must provide a valid email address to register with Zanata", + testPlanIds = 5681, testCaseIds = 5691) + @Test(timeout = MAX_SHORT_TEST_DURATION) public void invalidEmailRejection() throws Exception { - registerPage = registerPage.enterEmail("plaintext").registerFailure(); + registerPage = registerPage.enterEmail("notproper@").registerFailure(); + assertThat(registerPage.getErrors()) .contains(RegisterPage.MALFORMED_EMAIL_ERROR) .as("The email formation error is displayed"); + + registerPage = registerPage.clearFields() + .enterEmail("admin@example.com") + .registerFailure(); + + assertThat(registerPage.getErrors()) + .contains(RegisterPage.EMAIL_TAKEN) + .as("The user needs to provide a unique email address"); } } diff --git a/server/functional-test/src/test/java/org/zanata/feature/account/InactiveUserLoginTest.java b/server/functional-test/src/test/java/org/zanata/feature/account/InactiveUserLoginTest.java index d9b36e8170..86752638e2 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/account/InactiveUserLoginTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/account/InactiveUserLoginTest.java @@ -36,6 +36,7 @@ import org.zanata.workflow.LoginWorkFlow; import org.zanata.workflow.RegisterWorkFlow; import static org.assertj.core.api.Assertions.assertThat; +import static org.zanata.util.EmailQuery.LinkType.ACTIVATE; /** * @author Carlos Munoz @@ -63,9 +64,9 @@ public void verifyAccount() throws Exception { .isEqualTo("Zanata: Account is not activated") .as("The account is inactive"); WiserMessage message = hasEmailRule.getMessages().get(0); - assertThat(EmailQuery.hasActivationLink(message)).isTrue() + assertThat(EmailQuery.hasLink(message, ACTIVATE)).isTrue() .as("The email contains the activation link"); - String activationLink = EmailQuery.getActivationLink(message); + String activationLink = EmailQuery.getLink(message, ACTIVATE); SignInPage page = new BasicWorkFlow().goToUrl(activationLink, SignInPage.class); /* @@ -80,7 +81,8 @@ public void verifyAccount() throws Exception { "The user has validated their account and logged in"); } - @Trace(summary = "The user can resend the account activation email") + @Trace(summary = "The user can resend the account activation email", + testCaseIds = 5697) @Test(timeout = MAX_SHORT_TEST_DURATION) public void resendActivationEmail() throws Exception { String usernamepassword = "tester2"; @@ -95,10 +97,10 @@ public void resendActivationEmail() throws Exception { assertThat(hasEmailRule.getMessages().size()).isEqualTo(2) .as("A second email was sent"); WiserMessage message = hasEmailRule.getMessages().get(1); - assertThat(EmailQuery.hasActivationLink(message)).isTrue() + assertThat(EmailQuery.hasLink(message, ACTIVATE)).isTrue() .as("The second email contains the activation link"); homePage = new BasicWorkFlow() - .goToUrl(EmailQuery.getActivationLink(message), HomePage.class); + .goToUrl(EmailQuery.getLink(message, ACTIVATE), HomePage.class); /* * This fails in functional test, for reasons unknown * assertThat(homePage.getNotificationMessage()) @@ -111,7 +113,8 @@ public void resendActivationEmail() throws Exception { "The user has validated their account and logged in"); } - @Trace(summary = "The user can update the account activation email") + @Trace(summary = "The user can update the account activation email address", + testCaseIds = 5696) @Test(timeout = MAX_SHORT_TEST_DURATION) public void updateActivationEmail() throws Exception { String usernamepassword = "tester3"; @@ -133,10 +136,10 @@ public void updateActivationEmail() throws Exception { assertThat(message.getEnvelopeReceiver()) .isEqualTo("newtester@example.com") .as("The new email address is used"); - assertThat(EmailQuery.hasActivationLink(message)).isTrue() + assertThat(EmailQuery.hasLink(message, ACTIVATE)).isTrue() .as("The second email contains the activation link"); SignInPage page = new BasicWorkFlow().goToUrl( - EmailQuery.getActivationLink(message), SignInPage.class); + EmailQuery.getLink(message, ACTIVATE), SignInPage.class); /* * This fails in functional test, for reasons unknown * assertThat(homePage.getNotificationMessage()) diff --git a/server/functional-test/src/test/java/org/zanata/feature/account/comp/InactiveUserLoginCTest.java b/server/functional-test/src/test/java/org/zanata/feature/account/comp/InactiveUserLoginCTest.java new file mode 100644 index 0000000000..26d41b06f9 --- /dev/null +++ b/server/functional-test/src/test/java/org/zanata/feature/account/comp/InactiveUserLoginCTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors as indicated by the + * @author tags. See the copyright.txt file in the distribution for a full + * listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This software 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 Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this software; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF + * site: http://www.fsf.org. + */ +package org.zanata.feature.account.comp; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.zanata.feature.Trace; +import org.zanata.feature.testharness.ZanataTestCase; +import org.zanata.feature.testharness.TestPlan.DetailedTest; +import org.zanata.page.account.InactiveAccountPage; +import org.zanata.util.HasEmailRule; +import org.zanata.workflow.LoginWorkFlow; +import org.zanata.workflow.RegisterWorkFlow; + +/** + * @author Damian Jansen + * djansen@redhat.com + */ +@Category(DetailedTest.class) +public class InactiveUserLoginCTest extends ZanataTestCase { + private static final org.slf4j.Logger log = + org.slf4j.LoggerFactory.getLogger(InactiveUserLoginCTest.class); + + @Rule + public final HasEmailRule hasEmailRule = new HasEmailRule(); + + @Trace(summary = "The user can update the account activation email address", + testCaseIds = 5696) + @Test(timeout = MAX_SHORT_TEST_DURATION) + public void updateActivationEmail() throws Exception { + String usernamepassword = "tester3"; + new RegisterWorkFlow().registerInternal(usernamepassword, + usernamepassword, usernamepassword, + usernamepassword + "@example.com"); + InactiveAccountPage inactiveAccountPage = new LoginWorkFlow() + .signInInactive(usernamepassword, usernamepassword); + + assertThat(inactiveAccountPage.getTitle()) + .isEqualTo("Zanata: Account is not activated") + .as("The account is inactive"); + + inactiveAccountPage = inactiveAccountPage + .enterNewEmail("notproper@") + .updateEmailFailure(); + + assertThat(inactiveAccountPage.getErrors()) + .contains("not a well-formed email address"); + + inactiveAccountPage = inactiveAccountPage + .enterNewEmail("admin@example.com") + .updateEmailFailure(); + + assertThat(inactiveAccountPage.getErrors()) + .contains("This email address is already taken."); + } +} diff --git a/server/functional-test/src/test/java/org/zanata/feature/account/comp/RegisterCTest.java b/server/functional-test/src/test/java/org/zanata/feature/account/comp/RegisterCTest.java index 538e283ea6..c382dd73c9 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/account/comp/RegisterCTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/account/comp/RegisterCTest.java @@ -120,6 +120,58 @@ public void togglePasswordVisible() { .as("The password field did not lose the entered text"); } + @Trace(summary = "The user must provide a password to register via internal authentication", + testCaseIds = 5692) + @Test(timeout = MAX_SHORT_TEST_DURATION) + public void passwordLengthValidation() { + String longPass = makeString(1030); + assertThat(longPass.length()).isGreaterThan(1024); + + RegisterPage registerPage = homePage + .goToRegistration() + .enterName("jimmy") + .enterEmail("jimmy@jim.net") + .enterUserName("jimmy") + .enterPassword("A") + .registerFailure(); + + assertThat(registerPage.getErrors()) + .contains(RegisterPage.PASSWORD_LENGTH_ERROR) + .as("Password requires at least 6 characters"); + + registerPage = registerPage.enterPassword(longPass).registerFailure(); + + assertThat(registerPage.getErrors()) + .contains(RegisterPage.PASSWORD_LENGTH_ERROR) + .as("The user must enter a password of at most 1024 characters"); + } + + @Trace(summary = "The user must provide a name to register", + testPlanIds = 5681, testCaseIds = 5689) + @Test(timeout = ZanataTestCase.MAX_SHORT_TEST_DURATION) + public void userMustSpecifyAValidName() { + String longName = makeString(81); + assertThat(longName.length()).isGreaterThan(80); + + RegisterPage registerPage = homePage + .goToRegistration() + .enterName("A") + .enterUserName("usermustspecifyaname") + .enterEmail("userMustSpecifyAName@test.com") + .enterPassword("password") + .registerFailure(); + + assertThat(registerPage.getErrors()) + .contains(RegisterPage.USERDISPLAYNAME_LENGTH_ERROR) + .as("A name greater than 1 character must be specified"); + + registerPage = registerPage.enterName(longName).registerFailure(); + + assertThat(registerPage.getErrors()) + .contains(RegisterPage.USERDISPLAYNAME_LENGTH_ERROR) + .as("A name shorter than 81 characters is specified"); + } + @Trace(summary = "The user must provide a username to register", testPlanIds = 5681, testCaseIds = 5690) @Test(timeout = ZanataTestCase.MAX_SHORT_TEST_DURATION) @@ -163,4 +215,13 @@ private boolean containsUsernameError(List errors) { return errors.contains(RegisterPage.USERNAME_VALIDATION_ERROR) || errors.contains(RegisterPage.USERNAME_LENGTH_ERROR); } + + private String makeString(int length) { + char[] ret = new char[length]; + Random r = new Random(); + for (int i = 0; i < length; ++i) { + ret[i] = (char) (r.nextInt(26) + 'a'); + } + return String.valueOf(ret); + } } diff --git a/server/functional-test/src/test/java/org/zanata/feature/endtoend/UserEndToEndTest.java b/server/functional-test/src/test/java/org/zanata/feature/endtoend/UserEndToEndTest.java index 0d94fcfc40..2199c74d86 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/endtoend/UserEndToEndTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/endtoend/UserEndToEndTest.java @@ -53,6 +53,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.zanata.util.EmailQuery.LinkType.ACTIVATE; /** * This aim of this test is to provide a method of testing as many @@ -211,7 +212,7 @@ private SignInPage registerSuccessfully(RegisterPage registerPage) { private BasePage checkEmailAndFailToActivate() { WiserMessage message = hasEmailRule.getMessages().get(0); - String link = EmailQuery.getActivationLink(message); + String link = EmailQuery.getLink(message, ACTIVATE); boolean exceptionFound = false; BasePage basePage = null; try { @@ -225,7 +226,7 @@ private BasePage checkEmailAndFailToActivate() { private SignInPage checkEmailAndActivate() { WiserMessage message = hasEmailRule.getMessages().get(0); - String link = EmailQuery.getActivationLink(message); + String link = EmailQuery.getLink(message, ACTIVATE); SignInPage signInPage = new BasicWorkFlow().goToUrl(link.concat("?" + dswid), SignInPage.class); diff --git a/server/functional-test/src/test/java/org/zanata/feature/glossary/GlossaryAdminTest.java b/server/functional-test/src/test/java/org/zanata/feature/glossary/GlossaryAdminTest.java index 225f250d4d..64b46aa5e5 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/glossary/GlossaryAdminTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/glossary/GlossaryAdminTest.java @@ -20,7 +20,6 @@ */ package org.zanata.feature.glossary; -import org.hamcrest.Matchers; import org.junit.Test; import org.junit.experimental.categories.Category; import org.zanata.feature.Trace; @@ -29,7 +28,8 @@ import org.zanata.workflow.ClientWorkFlow; import java.io.File; import java.util.List; -import static org.hamcrest.MatcherAssert.assertThat; + +import static org.assertj.core.api.Assertions.assertThat; import static org.zanata.util.MavenHome.mvn; /** @@ -59,6 +59,6 @@ public void testGlossaryView() { List result = clientWorkFlow.callWithTimeout(projectRootPath, mvn() + " -e -U --batch-mode zanata:glossary-push -Dglossary.lang=hi -Dzanata.file=compendium.csv -Dzanata.userConfig=" + userConfigPath); - assertThat(clientWorkFlow.isPushSuccessful(result), Matchers.is(true)); + assertThat(clientWorkFlow.isPushSuccessful(result)).isTrue(); } } diff --git a/server/functional-test/src/test/java/org/zanata/feature/language/LanguageTestSuite.java b/server/functional-test/src/test/java/org/zanata/feature/language/LanguageTestSuite.java index 972d44ec95..28a593883e 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/language/LanguageTestSuite.java +++ b/server/functional-test/src/test/java/org/zanata/feature/language/LanguageTestSuite.java @@ -2,12 +2,15 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; +import org.zanata.feature.language.comp.LanguageCTest; @RunWith(Suite.class) @Suite.SuiteClasses({ AddLanguageTest.class, ContactLanguageTeamTest.class, - JoinLanguageTeamTest.class + JoinLanguageTeamTest.class, + //comprehensive test + LanguageCTest.class }) public class LanguageTestSuite { diff --git a/server/functional-test/src/test/java/org/zanata/feature/language/comp/LanguageCTest.java b/server/functional-test/src/test/java/org/zanata/feature/language/comp/LanguageCTest.java new file mode 100644 index 0000000000..9d61301dc2 --- /dev/null +++ b/server/functional-test/src/test/java/org/zanata/feature/language/comp/LanguageCTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors as indicated by the + * @author tags. See the copyright.txt file in the distribution for a full + * listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This software 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 Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this software; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF + * site: http://www.fsf.org. + */ + +package org.zanata.feature.language.comp; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.subethamail.wiser.WiserMessage; +import org.zanata.feature.Trace; +import org.zanata.feature.testharness.TestPlan; +import org.zanata.feature.testharness.ZanataTestCase; +import org.zanata.page.languages.LanguagePage; +import org.zanata.page.languages.LanguagesPage; +import org.zanata.util.HasEmailRule; +import org.zanata.workflow.BasicWorkFlow; +import org.zanata.workflow.LoginWorkFlow; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sachin Pathare spathare@redhat.com + */ +@Category(TestPlan.ComprehensiveTest.class) +public class LanguageCTest extends ZanataTestCase { + + @Rule + public final HasEmailRule emailRule = new HasEmailRule(); + + @Before + public void before() { + new BasicWorkFlow().goToHome().deleteCookiesAndRefresh(); + assertThat(new LoginWorkFlow().signIn("translator", "translator").loggedInAs()) + .isEqualTo("translator") + .as("translator is logged in"); + } + + @Trace(summary = "Translator can search for language", + testPlanIds = 5681, testCaseIds = {5786}) + @Test(timeout = MAX_SHORT_TEST_DURATION) + public void searchLanguage() throws Exception { + String language = "fr"; + LanguagesPage languagesPage = new BasicWorkFlow() + .goToHome() + .goToLanguages(); + + assertThat(languagesPage.getLanguageLocales()) + .contains(language) + .as("The language is listed"); + + } + + @Trace(summary = "Translator can request to join language team", + testPlanIds = 5681, testCaseIds = {5795, 5796}) + @Test(timeout = MAX_SHORT_TEST_DURATION) + public void requestToJoinLanguage() throws Exception { + String language = "en-US"; + LanguagePage languagePage = new BasicWorkFlow() + .goToHome() + .goToLanguages() + .gotoLanguagePage(language) + .requestToJoin() + .enterMessage("I want to join this language team") + .clickSend(); + + List messages = emailRule.getMessages(); + + assertThat(messages.size()) + .isGreaterThanOrEqualTo(1) + .as("One email was sent"); + + WiserMessage wiserMessage = messages.get(0); + + assertThat(wiserMessage.getEnvelopeReceiver()) + .isEqualTo("admin@example.com") + .as("The email recipient is the Coordinator"); + + String content = HasEmailRule.getEmailContent(wiserMessage); + + assertThat(content) + .contains("Dear Language Team Coordinator") + .contains("Zanata user \"translator\" with id \"translator\" is requesting to join ") + .as("The email is to the language team coordinator"); + + assertThat(languagePage.getNotificationMessage()) + .contains("Your message has been sent to the administrator") + .as("The user is informed the message was sent"); + + } + + @Trace(summary = "Translator can cancel request", + testPlanIds = 5681, testCaseIds = {5796}) + @Test(timeout = MAX_SHORT_TEST_DURATION) + public void cancelRequest() throws Exception { + String language = "en-US"; + LanguagePage languagePage = new BasicWorkFlow() + .goToHome() + .goToLanguages() + .gotoLanguagePage(language) + .requestToJoin() + .enterMessage("I want to join this language team") + .clickSend() + .cancelRequest(); + + assertThat(languagePage.getNotificationMessage()) + .contains("Request cancelled by translator") + .as("Request to join language team is cancel"); + } + + @Trace(summary = "Translator can leave language team", + testPlanIds = 5681, testCaseIds = {5800}) + @Test(timeout = MAX_SHORT_TEST_DURATION) + public void leaveLanguageTeam() throws Exception { + String language = "fr"; + LanguagePage languagePage = new BasicWorkFlow() + .goToHome() + .goToLanguages() + .gotoLanguagePage(language) + .leaveTeam(); + + assertThat(languagePage.getNotificationMessage()) + .contains("You have left the français language team") + .as("Leaved language team " + language); + } + +} diff --git a/server/functional-test/src/test/java/org/zanata/feature/rest/CopyTransTest.java b/server/functional-test/src/test/java/org/zanata/feature/rest/CopyTransTest.java index 71de407217..e4eb69fdb9 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/rest/CopyTransTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/rest/CopyTransTest.java @@ -20,7 +20,6 @@ */ package org.zanata.feature.rest; -import org.hamcrest.Matchers; import org.junit.Ignore; import org.junit.Test; import org.zanata.common.LocaleId; @@ -28,7 +27,6 @@ import org.zanata.rest.dto.resource.TranslationsResource; import org.zanata.util.ZanataRestCaller; -import static org.hamcrest.MatcherAssert.assertThat; import static org.zanata.util.ZanataRestCaller.*; /** @@ -66,8 +64,6 @@ public void testPushTranslationAndCopyTrans() { restCaller.asyncPushTarget(projectSlug, iterationSlug, docId, new LocaleId("pl"), transResource, "import", false); - assertThat(true, Matchers.is(true)); - // create another version restCaller.createProjectAndVersion(projectSlug, "2", projectType); restCaller.asyncPushSource(projectSlug, "2", sourceResource, false); @@ -110,8 +106,6 @@ void testPushTranslationRepeatedly() { localeId, transResource, "auto", false); restCaller.runCopyTrans(projectSlug, iterationSlug, docId); - assertThat(true, Matchers.is(true)); - // create some obsolete text flows Resource updatedSource = buildSourceResource(docId); TranslationsResource updatedTransResource = buildTranslationResource(); diff --git a/server/functional-test/src/test/java/org/zanata/feature/search/comp/GroupSearchCTest.java b/server/functional-test/src/test/java/org/zanata/feature/search/comp/GroupSearchCTest.java index 0ab9041aef..8f537c9713 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/search/comp/GroupSearchCTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/search/comp/GroupSearchCTest.java @@ -25,7 +25,6 @@ import org.junit.experimental.categories.Category; import org.zanata.feature.testharness.TestPlan; import org.zanata.feature.testharness.ZanataTestCase; -import org.zanata.page.dashboard.DashboardGroupsTab; import org.zanata.page.explore.ExplorePage; import org.zanata.page.groups.VersionGroupPage; import org.zanata.workflow.BasicWorkFlow; @@ -40,8 +39,6 @@ @Category(TestPlan.ComprehensiveTest.class) public class GroupSearchCTest extends ZanataTestCase { - private DashboardGroupsTab dashboardGroupsTab; - @Test(timeout = MAX_SHORT_TEST_DURATION) public void successfulGroupSearchAndDisplay() throws Exception { String groupID = "basic-group"; @@ -49,10 +46,10 @@ public void successfulGroupSearchAndDisplay() throws Exception { assertThat(new LoginWorkFlow().signIn("admin", "admin").loggedInAs()) .isEqualTo("admin") .as("Admin is logged in"); - dashboardGroupsTab = - new BasicWorkFlow().goToHome().goToMyDashboard().gotoGroupsTab(); - VersionGroupPage groupPage = dashboardGroupsTab + new BasicWorkFlow().goToHome() + .goToMyDashboard() + .gotoGroupsTab() .createNewGroup() .inputGroupId(groupID) .inputGroupName(groupName) @@ -80,7 +77,7 @@ public void unsuccessfulGroupSearch() throws Exception { ExplorePage explorePage = new BasicWorkFlow() .goToHome() .gotoExplore() - .enterSearch("group"); + .enterSearch("groop"); assertThat(explorePage.getGroupSearchResults().isEmpty()) .isTrue() diff --git a/server/functional-test/src/test/java/org/zanata/feature/security/SecurityTest.java b/server/functional-test/src/test/java/org/zanata/feature/security/SecurityTest.java index 6b67058822..3552fb95b5 100644 --- a/server/functional-test/src/test/java/org/zanata/feature/security/SecurityTest.java +++ b/server/functional-test/src/test/java/org/zanata/feature/security/SecurityTest.java @@ -29,13 +29,17 @@ import org.zanata.feature.Trace; import org.zanata.feature.testharness.TestPlan.DetailedTest; import org.zanata.feature.testharness.ZanataTestCase; +import org.zanata.page.account.EnterNewPasswordPage; import org.zanata.page.account.ResetPasswordPage; +import org.zanata.page.dashboard.DashboardBasePage; import org.zanata.page.utility.HomePage; +import org.zanata.util.EmailQuery; import org.zanata.util.HasEmailRule; import org.zanata.workflow.BasicWorkFlow; import org.zanata.workflow.LoginWorkFlow; import static org.assertj.core.api.Assertions.assertThat; +import static org.zanata.util.EmailQuery.LinkType.PASSWORD_RESET; /** * @author Damian Jansen djansen@redhat.com */ public class EmailQuery { + private static final org.slf4j.Logger log = + org.slf4j.LoggerFactory.getLogger(EmailQuery.class); - private static Pattern activationLink = Pattern - .compile("<(http://.+/activate/.+)>"); - private static Pattern validateLink = Pattern - .compile("<(http://.+/validate_email/.+)>"); - - public static boolean hasActivationLink(WiserMessage emailMessage) { - Matcher matcher = activationLink.matcher(HasEmailRule.getEmailContent(emailMessage)); - return matcher.find(); + public enum LinkType { + ACTIVATE, VALIDATE_EMAIL, PASSWORD_RESET } - public static String getActivationLink(WiserMessage emailMessage) { - Matcher matcher = activationLink.matcher(HasEmailRule.getEmailContent(emailMessage)); - assert matcher.find(); - return matcher.group(1); + private static Pattern getLinkRegex(LinkType type) { + return Pattern.compile("<(http://.+/" + type.name().toLowerCase() + "/.+)>"); } - public static boolean hasEmailVerificationLink(WiserMessage emailMessage) { - Matcher matcher = validateLink.matcher(HasEmailRule.getEmailContent(emailMessage)); + public static boolean hasLink(WiserMessage emailMessage, LinkType linkType) { + log.info("Query {} has a {} link", emailMessage, linkType.name()); + Pattern linkPattern = getLinkRegex(linkType); + Matcher matcher = linkPattern.matcher(HasEmailRule.getEmailContent(emailMessage)); return matcher.find(); } - public static String getEmailVerificationLink(WiserMessage emailMessage) { - Matcher matcher = validateLink.matcher(HasEmailRule.getEmailContent(emailMessage)); + public static String getLink(WiserMessage emailMessage, LinkType linkType) { + log.info("Get {} link from email {}", linkType.name(), emailMessage); + Pattern linkPattern = getLinkRegex(linkType); + Matcher matcher = linkPattern.matcher(HasEmailRule.getEmailContent(emailMessage)); assert matcher.find(); return matcher.group(1); } diff --git a/server/functional-test/src/test/java/org/zanata/util/ZanataRestCaller.java b/server/functional-test/src/test/java/org/zanata/util/ZanataRestCaller.java index 776e158947..62afd20c9a 100644 --- a/server/functional-test/src/test/java/org/zanata/util/ZanataRestCaller.java +++ b/server/functional-test/src/test/java/org/zanata/util/ZanataRestCaller.java @@ -136,9 +136,9 @@ public ContainerTranslationStatistics getStatistics(String projectSlug, } public void putSourceDocResource(String projectSlug, String iterationSlug, - String idNoSlash, Resource resource, boolean copytrans) { + String id, Resource resource, boolean copytrans) { restClientFactory.getSourceDocResourceClient(projectSlug, iterationSlug) - .putResource(idNoSlash, resource, Collections.emptySet(), + .putResource(id, resource, Collections.emptySet(), copytrans); } @@ -160,9 +160,9 @@ public static TextFlow buildTextFlow(String resId, String... contents) { } public void postTargetDocResource(String projectSlug, String iterationSlug, - String idNoSlash, LocaleId localeId, + String docId, LocaleId localeId, TranslationsResource translationsResource, String mergeType) { - asyncPushTarget(projectSlug, iterationSlug, idNoSlash, localeId, + asyncPushTarget(projectSlug, iterationSlug, docId, localeId, translationsResource, mergeType, false); } @@ -204,9 +204,10 @@ public void asyncPushSource(String projectSlug, String iterationSlug, AsyncProcessClient asyncProcessClient = restClientFactory.getAsyncProcessClient(); ProcessStatus processStatus = - asyncProcessClient.startSourceDocCreationOrUpdate( - sourceResource.getName(), projectSlug, iterationSlug, - sourceResource, Sets.newHashSet(), false); + asyncProcessClient.startSourceDocCreationOrUpdateWithDocId( + projectSlug, iterationSlug, + sourceResource, Sets.newHashSet(), + sourceResource.getName()); processStatus = waitUntilFinished(asyncProcessClient, processStatus.getUrl()); log.info("finished async source push ({}-{}): {}", projectSlug, iterationSlug, processStatus.getStatusCode()); @@ -249,9 +250,9 @@ public void asyncPushTarget(String projectSlug, String iterationSlug, AsyncProcessClient asyncProcessClient = restClientFactory.getAsyncProcessClient(); ProcessStatus processStatus = - asyncProcessClient.startTranslatedDocCreationOrUpdate(docId, + asyncProcessClient.startTranslatedDocCreationOrUpdateWithDocId( projectSlug, iterationSlug, localeId, transResource, - Collections. emptySet(), mergeType, + docId, Collections. emptySet(), mergeType, assignCreditToUploader); processStatus = waitUntilFinished(asyncProcessClient, processStatus.getUrl()); log.info("finished async translation({}-{}) push: {}", projectSlug, diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/UiMessages.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/UiMessages.java index 5384282aa7..d5393a610b 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/UiMessages.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/UiMessages.java @@ -20,8 +20,6 @@ */ package org.zanata.webtrans.client.resources; -import java.util.List; - import com.google.gwt.i18n.client.LocalizableResource.DefaultLocale; import com.google.gwt.i18n.client.LocalizableResource.Generate; import com.google.gwt.i18n.client.Messages; @@ -230,7 +228,7 @@ public interface UiMessages extends Messages { @DefaultMessage("Show as Diff") String diffModeAsDiff(); - @DefaultMessage("Highlight matches") + @DefaultMessage("Highlight Diff") String diffModeAsHighlight(); @DefaultMessage("Translation that contained validation warning or error.") @@ -250,4 +248,10 @@ public interface UiMessages extends Messages { @DefaultMessage("No content") String noContent(); + + @DefaultMessage("Matching") + String matching(); + + @DefaultMessage("Not Matching") + String notMatching(); } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/WebTransMessages.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/WebTransMessages.java index 36a3915fb3..55aab342df 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/WebTransMessages.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/resources/WebTransMessages.java @@ -426,7 +426,7 @@ String undoUnsuccessful(@PluralCount int unsuccessfulCount, String noMatch(); @DefaultMessage("Matching") - String noColor(); + String matching(); /** * Used for color legend in search and replace view diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.java index 46b51af25d..0973105c4f 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.java @@ -85,7 +85,7 @@ public void show(ShortcutContext context, DiffMode diffMode) { case TM: searchOnlyLabel.setText(messages.searchOnly()); tmOnlyLabel.setText(messages.tmOnly()); - matchLabel.setText(messages.noColor()); + matchLabel.setText(messages.matching()); searchOnlyDescription.setText(messages.tmInsertTagDesc()); tmOnlyDescription.setText(messages.tmDelTagDesc()); diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.ui.xml b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.ui.xml index 2ab9fccc29..068d2fcff0 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.ui.xml +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/DiffColorLegendPanel.ui.xml @@ -50,9 +50,9 @@ - + - + @@ -63,7 +63,7 @@ - + diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/Highlighting.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/Highlighting.java index 0cbb594b3d..58dde985a2 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/Highlighting.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/ui/Highlighting.java @@ -120,7 +120,7 @@ private static native String diffsToHtml(JavaScriptObject diffs) html[x] = '' + text + ''; break; case $wnd['DIFF_EQUAL']: - html[x] = '' + text + ''; + html[x] = '' + text + ''; break; } } @@ -150,12 +150,12 @@ private static native String diffsHighlight(JavaScriptObject diffs) '
'); switch (op) { case $wnd['DIFF_INSERT']: - html[x] = text; + html[x] = '' + text + ''; break; case $wnd['DIFF_DELETE']: break; case $wnd['DIFF_EQUAL']: - html[x] = '' + text + ''; + html[x] = text; break; } } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/util/ContentStateToStyleUtil.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/util/ContentStateToStyleUtil.java index 85de82e1a3..71a0ba8bd8 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/util/ContentStateToStyleUtil.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/util/ContentStateToStyleUtil.java @@ -39,7 +39,7 @@ public static String stateToStyle(ContentState state) { case Approved: return "txt--state-highlight"; case Rejected: - return "txt--state-danger"; + return "txt--state-warning"; } return styleNames; } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListDisplay.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListDisplay.java index 50fcd429cc..aa1af7ed0b 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListDisplay.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListDisplay.java @@ -86,7 +86,7 @@ interface Listener { void hideConfirmation(); - void updateFileDownloadProgress(int currentProgress, int maxProgress); + void updateFileDownloadProgress(long currentProgress, long maxProgress); void setDownloadInProgress(boolean inProgress); diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListView.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListView.java index 529dd8eb87..181a55ed3a 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListView.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/DocumentListView.java @@ -222,7 +222,7 @@ public void hideConfirmation() { @Override public void - updateFileDownloadProgress(int currentProgress, int maxProgress) { + updateFileDownloadProgress(long currentProgress, long maxProgress) { confirmationBox.setProgressMessage(currentProgress + " of " + maxProgress); } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.java index 33beb8a0aa..d652170541 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +import org.zanata.util.ShortString; import org.zanata.webtrans.client.keys.ShortcutContext; import org.zanata.webtrans.client.resources.UiMessages; import org.zanata.webtrans.client.ui.DiffColorLegendPanel; @@ -91,7 +92,7 @@ interface Styles extends CssResource { UiMessages messages; @UiField - InlineLabel searchOnly, tmOnly, diffLegendLabel; + InlineLabel searchOnly, tmOnly, diffLegendLabel, notMatching; @UiField FocusPanel diffLegend; @@ -235,10 +236,14 @@ private void updateDisplayMode() { if (determineDiffMode() == DiffMode.NORMAL) { tmOnly.removeStyleName("is-hidden"); searchOnly.removeStyleName("is-hidden"); + notMatching.removeStyleName("CodeMirror-searching"); + notMatching.setText(messages.matching()); diffLegendLabel.setText(messages.tmDiffHighlighting()); } else { tmOnly.addStyleName("is-hidden"); searchOnly.addStyleName("is-hidden"); + notMatching.addStyleName("CodeMirror-searching"); + notMatching.setText(messages.notMatching()); diffLegendLabel.setText(messages.tmHighlighting()); } } @@ -340,7 +345,7 @@ public void onClick(ClickEvent event) { Anchor infoCell = new Anchor(); if (item.getMatchType() == MatchType.Imported) { String originStr = Joiner.on(", ").join(item.getOrigins()); - infoCell.setText(shorten(originStr, 10)); + infoCell.setText(ShortString.shorten(originStr, 10)); infoCell.setTitle(originStr); } else { infoCell.setStyleName("i i--info txt--lead"); @@ -358,15 +363,6 @@ public void onClick(ClickEvent event) { } } - // TODO: Replace with ShortString::shorten when gwt can resolve the module - private String shorten(String s, int maxLength) { - String ellipsis = "…"; - if (s.length() <= maxLength) { - return s; - } - return s.substring(0, maxLength - ellipsis.length()) + ellipsis; - } - private static boolean odd(int n) { return n % 2 != 0; } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.ui.xml b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.ui.xml index 24df9cba3a..0a20cb89ac 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.ui.xml +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/client/view/TransMemoryView.ui.xml @@ -69,7 +69,7 @@
  • - Matching + Not matching TM only Search only diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryQuery.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryQuery.java index 87a0281741..2acad9ac03 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryQuery.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryQuery.java @@ -41,6 +41,7 @@ public class TransMemoryQuery implements IsSerializable, Serializable { private Condition project; private Condition document; private Condition res; + private List fromVersionIds; private Condition includeOwnTranslation = new Condition(true, null); @SuppressWarnings("unused") @@ -67,11 +68,13 @@ public TransMemoryQuery(List queries, SearchType searchType) { } public TransMemoryQuery(List queries, SearchType searchType, - Condition project, Condition document, Condition res) { + Condition project, Condition document, Condition res, + List fromVersionIds) { this(queries, searchType); this.project = project; this.document = document; this.res = res; + this.fromVersionIds = fromVersionIds; } public TransMemoryQuery(String query, SearchType searchType, @@ -110,6 +113,10 @@ public SearchType getSearchType() { return searchType; } + public List getFromVersionIds() { + return fromVersionIds; + } + @Override public String toString() { return "TransMemoryQuery{" + "searchType=" + searchType + ", queries=" diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryResultItem.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryResultItem.java index 8876facc41..b62cef488c 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryResultItem.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/model/TransMemoryResultItem.java @@ -54,6 +54,7 @@ public enum MatchType { // The optional origin identifiers for this result (i.e. A Trans memory name) private List origins; private ArrayList sourceIdList = new ArrayList(); + private Long fromVersionId; // for GWT @SuppressWarnings("unused") @@ -65,14 +66,17 @@ private TransMemoryResultItem() { * @param targetContents * @param relevanceScore * @param similarityPercent + * @param fromVersionId */ public TransMemoryResultItem(ArrayList sourceContents, ArrayList targetContents, MatchType matchType, - double relevanceScore, double similarityPercent) { + double relevanceScore, double similarityPercent, + Long fromVersionId) { super(relevanceScore, similarityPercent); this.sourceContents = sourceContents; this.targetContents = targetContents; this.matchType = matchType; + this.fromVersionId = fromVersionId; this.origins = new ArrayList(); } @@ -134,4 +138,7 @@ public void addSourceId(Long sourceId) { this.sourceIdList.add(sourceId); } + public Long getFromVersionId() { + return fromVersionId; + } } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/HasTMMergeCriteria.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/HasTMMergeCriteria.java new file mode 100644 index 0000000000..5ba6520d6b --- /dev/null +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/HasTMMergeCriteria.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.zanata.webtrans.shared.rest.dto; + +import java.util.List; + +import org.zanata.webtrans.shared.model.ProjectIterationId; +import org.zanata.webtrans.shared.rpc.MergeRule; + +public interface HasTMMergeCriteria { + int getThresholdPercent(); + + MergeRule getDifferentProjectRule(); + + MergeRule getDifferentDocumentRule(); + + MergeRule getDifferentContextRule(); + + MergeRule getImportedMatchRule(); + +} diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/TransMemoryMergeRequest.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/TransMemoryMergeRequest.java index 20e26ef448..155b39dc44 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/TransMemoryMergeRequest.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rest/dto/TransMemoryMergeRequest.java @@ -20,6 +20,8 @@ */ package org.zanata.webtrans.shared.rest.dto; +import java.util.List; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.zanata.common.LocaleId; import org.zanata.webtrans.shared.auth.EditorClientId; @@ -30,7 +32,7 @@ /** * @author Patrick Huang pahuang@redhat.com */ -public class TransMemoryMergeRequest { +public class TransMemoryMergeRequest implements HasTMMergeCriteria { @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "For future implement") public EditorClientId editorClientId; @@ -71,23 +73,29 @@ public TransMemoryMergeRequest( public TransMemoryMergeRequest() { } + @Override public int getThresholdPercent() { return thresholdPercent; } + @Override public MergeRule getDifferentProjectRule() { return differentProjectRule; } + @Override public MergeRule getDifferentDocumentRule() { return differentDocumentRule; } + @Override public MergeRule getDifferentContextRule() { return differentContextRule; } + @Override public MergeRule getImportedMatchRule() { return importedMatchRule; } + } diff --git a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rpc/GetDownloadAllFilesProgressResult.java b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rpc/GetDownloadAllFilesProgressResult.java index 8eb684cb83..23df255aff 100644 --- a/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rpc/GetDownloadAllFilesProgressResult.java +++ b/server/gwt-editor/src/main/java/org/zanata/webtrans/shared/rpc/GetDownloadAllFilesProgressResult.java @@ -4,26 +4,26 @@ public class GetDownloadAllFilesProgressResult implements DispatchResult { private static final long serialVersionUID = 1L; - private int currentProgress; - private int maxProgress; + private long currentProgress; + private long maxProgress; private String downloadId; @SuppressWarnings("unused") private GetDownloadAllFilesProgressResult() { } - public GetDownloadAllFilesProgressResult(int currentProgress, - int maxProgress, String downloadId) { + public GetDownloadAllFilesProgressResult(long currentProgress, + long maxProgress, String downloadId) { this.maxProgress = maxProgress; this.currentProgress = currentProgress; this.downloadId = downloadId; } - public int getCurrentProgress() { + public long getCurrentProgress() { return currentProgress; } - public int getMaxProgress() { + public long getMaxProgress() { return maxProgress; } diff --git a/server/gwt-editor/src/main/webapp/webtrans/Application.xhtml b/server/gwt-editor/src/main/webapp/webtrans/Application.xhtml index 79c7a1cebc..7db1107817 100644 --- a/server/gwt-editor/src/main/webapp/webtrans/Application.xhtml +++ b/server/gwt-editor/src/main/webapp/webtrans/Application.xhtml @@ -19,13 +19,13 @@ #{msgs['jsf.PageTitle']} - + - - + + diff --git a/server/pom.xml b/server/pom.xml index b0dac85393..32f5af39e7 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -654,13 +654,6 @@ 2.2.1 - - - org.zanata - zanata-assets - ${zanata.assets.version} - - org.zanata zanata-frontend diff --git a/server/zanata-frontend/.gitattributes b/server/zanata-frontend/.gitattributes new file mode 100644 index 0000000000..e1898b8d7c --- /dev/null +++ b/server/zanata-frontend/.gitattributes @@ -0,0 +1,3 @@ +*.js text eol=lf +*.jsx text eol=lf +*.jsx.src text eol=lf diff --git a/server/zanata-frontend/src/frontend/app/actions/common-actions.js b/server/zanata-frontend/src/frontend/app/actions/common-actions.js index b2f296c815..ee0ce904fa 100644 --- a/server/zanata-frontend/src/frontend/app/actions/common-actions.js +++ b/server/zanata-frontend/src/frontend/app/actions/common-actions.js @@ -31,6 +31,7 @@ export const getHeaders = () => { export const getJsonHeaders = () => { let headers = getHeaders() headers['Accept'] = 'application/json' + headers['Content-Type'] = 'application/json' return headers } diff --git a/server/zanata-frontend/src/frontend/app/actions/tmx-actions.js b/server/zanata-frontend/src/frontend/app/actions/tmx-actions.js index b4106d6d77..8aabce22ef 100644 --- a/server/zanata-frontend/src/frontend/app/actions/tmx-actions.js +++ b/server/zanata-frontend/src/frontend/app/actions/tmx-actions.js @@ -85,57 +85,57 @@ const getTMX = (endpoint) => { } } -// get all source language in all active documents -const fetchAllSourceLanguages = () => { - const endpoint = apiUrl + '/locales/source' - return fetchSourceLanguages(endpoint) -} - -// get all source language in all active documents in project -const fetchProjectSourceLanguages = (project) => { - const endpoint = apiUrl + '/projects/p/' + project + '/locales/source' - return fetchSourceLanguages(endpoint) -} - -// get all source language in all active documents in project version -const fetchVersionSourceLanguages = (project, version) => { - const endpoint = apiUrl + '/projects/p/' + project + - '/iterations/i/' + version + '/locales/source' - return fetchSourceLanguages(endpoint) -} - -export const tmxInitialLoad = (type, project, version) => { +export const tmxInitialLoad = (project, version) => { return (dispatch, getState) => { + let type + if ((project || !isUndefined(project)) && + (!version || isUndefined(version))) { + type = TMX_TYPE[1] // project type + } else if ((project || !isUndefined(project)) && + (version || !isUndefined(version))) { + type = TMX_TYPE[2] // project version type + } else { + type = TMX_TYPE[0] // all type + } dispatch(setInitialState({type, project, version})) + let endpoint switch (type) { case TMX_TYPE[0]: - dispatch(fetchAllSourceLanguages()) + // get all source language in all active documents + endpoint = apiUrl + '/locales/source' + dispatch(fetchSourceLanguages(endpoint)) break case TMX_TYPE[1]: - dispatch(fetchProjectSourceLanguages(project)) + // get all source language in all active documents in project + endpoint = apiUrl + '/projects/p/' + project + '/locales/source' + dispatch(fetchSourceLanguages(endpoint)) break case TMX_TYPE[2]: - dispatch(fetchVersionSourceLanguages(project, version)) + // get all source language in all active documents in project version + endpoint = apiUrl + '/projects/p/' + project + + '/iterations/i/' + version + '/locales/source' + dispatch(fetchSourceLanguages(endpoint)) break } } } -export const exportTMX = () => { +export const exportTMX = (localeId) => { return (dispatch, getState) => { const state = getState().tmx const type = state.tmxExport.type let endpoint switch (type) { case TMX_TYPE[0]: - endpoint = apiUrl + '/tm/all' + endpoint = apiUrl + '/tm/all?srcLocale=' + localeId break case TMX_TYPE[1]: - endpoint = apiUrl + '/tm/projects/' + state.project + endpoint = apiUrl + '/tm/projects/' + state.project + + '?srcLocale=' + localeId break case TMX_TYPE[2]: endpoint = apiUrl + '/tm/projects/' + state.project + - '/iterations/' + state.version + '/iterations/' + state.version + '?srcLocale=' + localeId break } dispatch(getTMX(endpoint)) diff --git a/server/zanata-frontend/src/frontend/app/actions/version-action-types.js b/server/zanata-frontend/src/frontend/app/actions/version-action-types.js new file mode 100644 index 0000000000..45ec97d684 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/actions/version-action-types.js @@ -0,0 +1,21 @@ +export const TOGGLE_TM_MERGE_MODAL = 'TOGGLE_TM_MERGE_MODAL' +export const VERSION_LOCALES_REQUEST = 'VERSION_LOCALES_REQUEST' +export const VERSION_LOCALES_SUCCESS = 'VERSION_LOCALES_SUCCESS' +export const VERSION_LOCALES_FAILURE = 'VERSION_LOCALES_FAILURE' +export const PROJECT_PAGE_REQUEST = 'PROJECT_PAGE_REQUEST' +export const PROJECT_PAGE_SUCCESS = 'PROJECT_PAGE_SUCCESS' +export const PROJECT_PAGE_FAILURE = 'PROJECT_PAGE_FAILURE' + +export const VERSION_TM_MERGE_REQUEST = 'VERSION_TM_MERGE_REQUEST' +export const VERSION_TM_MERGE_SUCCESS = 'VERSION_TM_MERGE_SUCCESS' +export const VERSION_TM_MERGE_FAILURE = 'VERSION_TM_MERGE_FAILURE' + +export const QUERY_TM_MERGE_PROGRESS_REQUEST = 'QUERY_TM_MERGE_PROGRESS_REQUEST' +export const QUERY_TM_MERGE_PROGRESS_SUCCESS = 'QUERY_TM_MERGE_PROGRESS_SUCCESS' +export const QUERY_TM_MERGE_PROGRESS_FAILURE = 'QUERY_TM_MERGE_PROGRESS_FAILURE' + +export const TM_MERGE_CANCEL_REQUEST = 'TM_MERGE_CANCEL_REQUEST' +export const TM_MERGE_CANCEL_SUCCESS = 'TM_MERGE_CANCEL_SUCCESS' +export const TM_MERGE_CANCEL_FAILURE = 'TM_MERGE_CANCEL_FAILURE' + +export const TM_MERGE_PROCESS_FINISHED = 'TM_MERGE_PROCESS_FINISHED' diff --git a/server/zanata-frontend/src/frontend/app/actions/version-actions.js b/server/zanata-frontend/src/frontend/app/actions/version-actions.js new file mode 100644 index 0000000000..767d9c51d1 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/actions/version-actions.js @@ -0,0 +1,165 @@ +import { CALL_API, getJSON } from 'redux-api-middleware' +import { createAction } from 'redux-actions' +import { + getJsonHeaders, + buildAPIRequest +} from './common-actions' +import { apiUrl } from '../config' +import {replace} from 'lodash' + +import { + TOGGLE_TM_MERGE_MODAL, + VERSION_LOCALES_REQUEST, + VERSION_LOCALES_SUCCESS, + VERSION_LOCALES_FAILURE, + PROJECT_PAGE_REQUEST, + PROJECT_PAGE_SUCCESS, + PROJECT_PAGE_FAILURE, + VERSION_TM_MERGE_REQUEST, + VERSION_TM_MERGE_SUCCESS, + VERSION_TM_MERGE_FAILURE, + QUERY_TM_MERGE_PROGRESS_REQUEST, + QUERY_TM_MERGE_PROGRESS_SUCCESS, + QUERY_TM_MERGE_PROGRESS_FAILURE, + TM_MERGE_CANCEL_REQUEST, + TM_MERGE_CANCEL_SUCCESS, + TM_MERGE_CANCEL_FAILURE, + TM_MERGE_PROCESS_FINISHED +} from './version-action-types' + +/** Open or close the TM Merge modal */ +export const toggleTMMergeModal = + createAction(TOGGLE_TM_MERGE_MODAL) + +/** + * Fetch project version specific locales from database + * + * @param project projectSlug + * @param version versionSlug + * */ +export const fetchVersionLocales = (project, version) => { + const endpoint = `${apiUrl}/project/${project}/version/${version}/locales` + const apiTypes = [ + VERSION_LOCALES_REQUEST, + VERSION_LOCALES_SUCCESS, + VERSION_LOCALES_FAILURE + ] + return { + [CALL_API]: buildAPIRequest(endpoint, 'GET', getJsonHeaders(), apiTypes) + } +} + +/** + * Fetch projects to merge from database + * + * @param projectSearchTerm to filter results + * */ +export const fetchProjectPage = (projectSearchTerm) => { + // used for success/failure to ensure the most recent results are used + const timestamp = Date.now() + const endpoint = + `${apiUrl}/search/projects?q=${projectSearchTerm}&includeVersion=true` + const apiTypes = [ + PROJECT_PAGE_REQUEST, + { + type: PROJECT_PAGE_SUCCESS, + meta: {timestamp}, + payload: (action, state, res) => { + return getJSON(res).then((json) => json.results) + } + }, + { + type: PROJECT_PAGE_FAILURE, + meta: {timestamp} + } + ] + return { + [CALL_API]: buildAPIRequest(endpoint, 'GET', getJsonHeaders(), apiTypes) + } +} + +// convert merge option to MergeRule enum value +const fuzzyOrRejectMergeRule = (isAccept) => isAccept ? 'FUZZY' : 'REJECT' + +// convert project version to string representation +const toProjectVersionString = (projectVersion) => { + return `${projectVersion.projectSlug}/${projectVersion.version.id}` +} + +/** + * @param {string} projectSlug target project slug + * @param {string} versionSlug target version slug + * @param {{ + * matchPercentage: number, + * differentDocId: boolean, + * differentContext: boolean, + * fromImportedTM: boolean, + * selectedLanguage: Object.<{localeId: string, displayName: string}>, + * selectedVersions: Array.<{projectSlug: string, version: {id: string}}> + * }} mergeOptions + * @returns redux api action object + */ +export function mergeVersionFromTM (projectSlug, versionSlug, mergeOptions) { + const endpoint = + `${apiUrl}/project/${projectSlug}/version/${versionSlug}/tm-merge` + const types = [VERSION_TM_MERGE_REQUEST, + { + type: VERSION_TM_MERGE_SUCCESS, + payload: (action, state, res) => { + const contentType = res.headers.get('Content-Type') + if (contentType && ~contentType.indexOf('json')) { + // Just making sure res.json() does not raise an error + return res.json().then((json) => { + const cancelUrl = replace(json.url, + '/rest/process/', '/rest/process/cancel/') + return {...json, cancelUrl} + }) + } + } + }, VERSION_TM_MERGE_FAILURE] + const { + selectedLanguage: {localeId}, + matchPercentage, + differentDocId, + differentContext, + fromImportedTM, + selectedVersions + } = mergeOptions + const body = { + localeId: localeId, + thresholdPercent: matchPercentage, + differentDocumentRule: fuzzyOrRejectMergeRule(differentDocId), + differentContextRule: fuzzyOrRejectMergeRule(differentContext), + importedMatchRule: fuzzyOrRejectMergeRule(fromImportedTM), + fromProjectVersions: selectedVersions.map(toProjectVersionString) + } + const apiRequest = buildAPIRequest( + endpoint, 'POST', getJsonHeaders(), types, JSON.stringify(body) + ) + return { + [CALL_API]: apiRequest + } +} + +export function queryTMMergeProgress (url) { + const types = [QUERY_TM_MERGE_PROGRESS_REQUEST, + QUERY_TM_MERGE_PROGRESS_SUCCESS, + QUERY_TM_MERGE_PROGRESS_FAILURE] + return { + [CALL_API]: buildAPIRequest(url, 'GET', getJsonHeaders(), types) + } +} + +export function cancelTMMergeRequest (url) { + const types = [ + TM_MERGE_CANCEL_REQUEST, + TM_MERGE_CANCEL_SUCCESS, + TM_MERGE_CANCEL_FAILURE + ] + return { + [CALL_API]: buildAPIRequest(url, 'POST', getJsonHeaders(), types) + } +} + +export const currentTMMergeProcessFinished = + createAction(TM_MERGE_PROCESS_FINISHED) diff --git a/server/zanata-frontend/src/frontend/app/actions/version-actions.test.js b/server/zanata-frontend/src/frontend/app/actions/version-actions.test.js new file mode 100644 index 0000000000..ca848c365f --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/actions/version-actions.test.js @@ -0,0 +1,74 @@ +jest.disableAutomock() +import { CALL_API } from 'redux-api-middleware' +import { + fetchVersionLocales, + fetchProjectPage, + mergeVersionFromTM, + queryTMMergeProgress, + cancelTMMergeRequest +} from './version-actions' + +describe('version-action test', () => { + it('can fetch version locales', () => { + const apiAction = fetchVersionLocales('meikai', 'ver1') + expect(apiAction[CALL_API].endpoint).toEqual( + '/rest/project/meikai/version/ver1/locales' + ) + }) + it('can fetch project pages', () => { + const apiAction = fetchProjectPage('meikai') + expect(apiAction[CALL_API].endpoint).toEqual( + '/rest/search/projects?q=meikai&includeVersion=true' + ) + }) + it('can build url endpoint from merge options', () => { + const mergeOptions = { + selectedLanguage: { + displayName: "Japanese", + enabled: true, + enabledByDefault: true, + localeId: "ja", + nativeName: "日本語", + pluralForms: "nplurals=1; plural=0" + }, + matchPercentage: 100, + differentDocId: false, + differentContext: false, + fromImportedTM: false, + selectedVersions: [ + { + projectSlug: "meikai1", + version: { + id: "ver1", + status: "ACTIVE" + } + }, + { + projectSlug: "meikai2", + version: { + id: "ver2", + status: "ACTIVE" + } + } + ] + } + const apiAction = mergeVersionFromTM('meikai', 'ver1', mergeOptions) + expect(apiAction[CALL_API].endpoint).toEqual( + '/rest/project/meikai/version/ver1/tm-merge' + ) + }) + it('can query TM merge progress', () => { + const url = '/rest/process/key/TMMergeForVerKey-1-ja' + const apiAction = queryTMMergeProgress(url) + expect(apiAction[CALL_API].endpoint).toEqual( + '/rest/process/key/TMMergeForVerKey-1-ja' + ) + }) + it('can cancel the TM merge process', () => { + const url = '/rest/cancel/process/key/TMMergeForVerKey-1-ja' + const apiAction = cancelTMMergeRequest(url) + expect(apiAction[CALL_API].endpoint).toEqual( + '/rest/cancel/process/key/TMMergeForVerKey-1-ja' + ) + }) +}) diff --git a/server/zanata-frontend/src/frontend/app/components/DraggableVersionPanels/DraggableVersionPanels.test.js b/server/zanata-frontend/src/frontend/app/components/DraggableVersionPanels/DraggableVersionPanels.test.js new file mode 100644 index 0000000000..32d1eba4c3 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/DraggableVersionPanels/DraggableVersionPanels.test.js @@ -0,0 +1,88 @@ +/* global jest describe it expect */ +jest.disableAutomock() + +import React from 'react' +import ReactDOMServer from 'react-dom/server' +import DraggableVersionPanels, {Item, DragHandle, tooltipSort} from '.' +import {Button, ListGroup, ListGroupItem, OverlayTrigger} from 'react-bootstrap' +import {Icon, LockIcon} from '../../components' + +const callback = function (e) {} + +describe('DraggableVersionPanels', () => { + it('can render a draggable Item', () => { + const version = { + projectSlug: 'meikai1', + version: { + id: 'ver1', + status: 'ACTIVE' + } + } + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + const expected = ReactDOMServer.renderToStaticMarkup( + + + {'ver1'} {'meikai1'} + + {" "} + + + ) + expect(actual).toEqual(expected) + }) + it('can render DraggableVersionPanels', () => { + const someVersions = [{ + projectSlug: 'meikai1', + version: { + id: 'ver1', + status: 'ACTIVE' + } + }, + { + projectSlug: 'meikai2', + version: { + id: 'ver2', + status: 'ACTIVE' + } + }] + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + const expected = ReactDOMServer.renderToStaticMarkup( + +
    + + Adjust priority of selected versions +
    + (best first) + + + + + +
    +
    + ) + expect(actual).toEqual(expected) + }) + it('returns an empty span if there are no selectedVersions', () => { + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + expect(actual).toEqual('') + }) +}) diff --git a/server/zanata-frontend/src/frontend/app/components/DraggableVersionPanels/index.js b/server/zanata-frontend/src/frontend/app/components/DraggableVersionPanels/index.js new file mode 100644 index 0000000000..967bca3534 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/DraggableVersionPanels/index.js @@ -0,0 +1,108 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {FromProjectVersionType} from '../../utils/prop-types-util' +import {Icon, LockIcon} from '../../components' +import { + SortableContainer, + SortableElement, + SortableHandle +} from 'react-sortable-hoc' +import { + Button, + ListGroup, + ListGroupItem, + Tooltip, + OverlayTrigger +} from 'react-bootstrap' + +const DO_NOT_RENDER = null + +export const tooltipSort = ( + Best match will be chosen based on the priority of + selected projects. Exact matches take precendence. + +) + +export const DragHandle = SortableHandle(() => + ) + +export class Item extends Component { + static propTypes = { + value: FromProjectVersionType.isRequired, + removeVersion: PropTypes.func.isRequired + } + removeVersion = () => { + const { value: { version, projectSlug } } = this.props + this.props.removeVersion(projectSlug, version) + } + render () { + const { value: { version, projectSlug } } = this.props + return + + {version.id} {projectSlug} + + {" "} + + + } +} +const SortableItem = SortableElement(Item) + +class Items extends Component { + static propTypes = { + items: PropTypes.arrayOf(FromProjectVersionType).isRequired, + removeVersion: PropTypes.func.isRequired + } + render () { + const { items, removeVersion } = this.props + const sortableItems = items.map((value, index) => ( + )) + return ( +
    + + Adjust priority of selected versions +
    + + (best first) + + + + + {sortableItems} +
    + ) + } +} + +const SortableList = SortableContainer(Items) + +/** + * Draggable version priority list + */ +class DraggableVersionPanels extends Component { + static propTypes = { + selectedVersions: PropTypes.arrayOf(FromProjectVersionType).isRequired, + onDraggableMoveEnd: PropTypes.func.isRequired, + removeVersion: PropTypes.func.isRequired + } + render () { + if (this.props.selectedVersions.length === 0) { + return DO_NOT_RENDER + } + return ( + + + + ) + } +} + +export default DraggableVersionPanels diff --git a/server/zanata-frontend/src/frontend/app/components/LockIcon/LockIcon.test.js b/server/zanata-frontend/src/frontend/app/components/LockIcon/LockIcon.test.js new file mode 100644 index 0000000000..c8ad826fe3 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/LockIcon/LockIcon.test.js @@ -0,0 +1,25 @@ +/* global jest describe it expect */ +jest.disableAutomock() + +import React from 'react' +import ReactDOMServer from 'react-dom/server' +import LockIcon from '.' +import {Icon} from '../../components' + +describe('LockIcon', () => { + it('renders a LockIcon when given a READONLY status', () => { + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + const expected = ReactDOMServer.renderToStaticMarkup( + + ) + expect(actual).toEqual(expected) + }) + it('renders an empty span when given an ACTIVE status', () => { + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + expect(actual).toEqual('') + }) +}) diff --git a/server/zanata-frontend/src/frontend/app/components/LockIcon/index.js b/server/zanata-frontend/src/frontend/app/components/LockIcon/index.js new file mode 100644 index 0000000000..63ddf72066 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/LockIcon/index.js @@ -0,0 +1,29 @@ +import React from 'react' +import {Tooltip, OverlayTrigger} from 'react-bootstrap' +import {Icon} from '../../components' +import {entityStatusPropType} from '../../utils/prop-types-util' +import {isEntityStatusReadOnly} from '../../utils/EnumValueUtils' + +const DO_NOT_RENDER = null + +/** + * Version Lock Icon with tooltip + * + * @param status + * @returns {XML} + */ +const LockIcon = ({status}) => { + const tooltipReadOnly = Read only + return isEntityStatusReadOnly(status) + ? ( + + + + ) + : DO_NOT_RENDER +} +LockIcon.propTypes = { + status: entityStatusPropType +} + +export default LockIcon diff --git a/server/zanata-frontend/src/frontend/app/components/Nav/index.jsx b/server/zanata-frontend/src/frontend/app/components/Nav/index.jsx index 234ea8d2db..b56049801e 100644 --- a/server/zanata-frontend/src/frontend/app/components/Nav/index.jsx +++ b/server/zanata-frontend/src/frontend/app/components/Nav/index.jsx @@ -10,11 +10,9 @@ const dswid = getDswid() /** * Item properties: * - * - link: path to use for JSF pages, or when internalLink is not specified - * OR a key in props.links to look up the path to use. - * (FIXME inconsistent and error-prone, split this into 2 properties) - * - internalLink: path to use when the Nav component is part of the main - * frontend app + * - link: URL path for the page. + * - jsPage: indicator for ReactJS page. Force to use href for their links. + * */ const items = [ { @@ -28,7 +26,7 @@ const items = [ { icon: 'search', link: '/explore' + dswid, - internalLink: '/explore', + jsPage: true, title: 'Explore', auth: 'public', id: 'nav_search' @@ -61,7 +59,7 @@ const items = [ small: true, icon: 'user', link: '/profile' + dswid, - internalLink: '/profile', + jsPage: true, title: 'Profile', auth: 'loggedin', id: 'nav_profile' @@ -69,7 +67,7 @@ const items = [ { icon: 'glossary', link: '/glossary' + dswid, - internalLink: '/glossary', + jsPage: true, title: 'Glossary', auth: 'loggedin', id: 'nav_glossary' @@ -77,7 +75,7 @@ const items = [ { icon: 'language', link: '/languages' + dswid, - internalLink: '/languages', + jsPage: true, title: 'Languages', auth: 'loggedin', id: 'nav_language' @@ -151,14 +149,12 @@ const Nav = ({ : (links.context + item.link) } else { // react pages, /app/index.xhtml - link = item.internalLink - ? item.internalLink - : (links[item.link] - ? (links.context + links[item.link]) - : (links.context + item.link)) + link = links[item.link] + ? (links.context + links[item.link]) + : (links.context + item.link) } - const useHref = isJsfPage || !item.internalLink + const useHref = isJsfPage || !item.jsPage let linkWithoutDswid = link.replace(dswid, '') /** diff --git a/server/zanata-frontend/src/frontend/app/components/ProgressBar/CancellableProgressBar.js b/server/zanata-frontend/src/frontend/app/components/ProgressBar/CancellableProgressBar.js new file mode 100644 index 0000000000..65c369f021 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/ProgressBar/CancellableProgressBar.js @@ -0,0 +1,74 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import { + Button, ProgressBar +} from 'react-bootstrap' +import { processStatusType } from '../../utils/prop-types-util' +import { isProcessEnded } from '../../utils/EnumValueUtils' + +/** + * This component can be used to show progress of a background task running on + * server. e.g. CopyTrans, TM merge, Copy Version etc. + * It offers a 'Cancel Operation' button to stop the task on server. + * It should also track the progress of the task (before we have websocket ready + * on server, we have to poll the server to get progress) + */ +class CancellableProgressBar extends Component { + static propTypes = { + heading: PropTypes.string, + onCancelOperation: PropTypes.func.isRequired, + processStatus: processStatusType.isRequired, + queryProgress: PropTypes.func.isRequired, + buttonLabel: PropTypes.string.isRequired + } + static defaultProps = { + heading: '', + buttonLabel: 'Cancel Operation' + } + constructor (props) { + super(props) + this.state = { + // helper state to stop the loop in dev mode (with chrome react add-on) + stopTimer: false + } + } + queryProgressLoop = () => { + this.props.queryProgress() + this.timer = setTimeout(this.queryProgressLoop, 750) + } + stopTimer = () => { + if (this.timer) { + clearTimeout(this.timer) + } + } + componentDidMount () { + this.queryProgressLoop() + } + componentWillUpdate (nextProp, nextState) { + if (isProcessEnded(nextProp.processStatus) || nextState.stopTimer) { + this.stopTimer() + } + } + componentWillUnmount () { + this.stopTimer() + } + render () { + const { + heading, onCancelOperation, processStatus, buttonLabel + } = this.props + return ( +
    + + +
    + ) + } +} + +export default CancellableProgressBar diff --git a/server/zanata-frontend/src/frontend/app/components/ProgressBar/CancellableProgressBar.test.js b/server/zanata-frontend/src/frontend/app/components/ProgressBar/CancellableProgressBar.test.js new file mode 100644 index 0000000000..b6d836dd9f --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/ProgressBar/CancellableProgressBar.test.js @@ -0,0 +1,83 @@ +/* global jest describe it expect */ +jest.disableAutomock() + +import React from 'react' +import ReactDOMServer from 'react-dom/server' +import CancellableProgressBar from './CancellableProgressBar' +import { ProgressBar } from 'react-bootstrap' +import { isProcessEnded } from '../../utils/EnumValueUtils' + +const callback = () => {} + +describe('CancellableProgressBar', () => { + it('can render CancellableProgressBar markup', () => { + const processStatus = { + url: '/rest/process/key/TMMergeForVerKey-1-ja', + percentageComplete: 0, + statusCode: 'Running' + } + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + const expected = ReactDOMServer.renderToStaticMarkup( +
    + + +
    + ) + expect(actual).toEqual(expected) + }) + it('detects loading process completion', () => { + const cancelledStatus = { + url: '/rest/process/key/TMMergeForVerKey-1-ja', + percentageComplete: 0, + statusCode: 'Cancelled' + } + // Test the CancellableProgressBar markup cancel button is disabled + expect(ReactDOMServer.renderToStaticMarkup( + + )).toEqual(ReactDOMServer.renderToStaticMarkup( +
    + + +
    + )) + }) + + it('detects loading process cancellation', () => { + // Testing the isProcessEnded utils function + const cancelledStatus1 = { + url: '/rest/process/key/TMMergeForVerKey-1-ja', + percentageComplete: 0, + statusCode: 'Cancelled' + } + expect(isProcessEnded(cancelledStatus1)).toEqual(true) + const cancelledStatus2 = { + url: '/rest/process/key/TMMergeForVerKey-1-ja', + percentageComplete: 66, + statusCode: 'Cancelled' + } + expect(isProcessEnded(cancelledStatus2)).toEqual(true) + const cancelledStatus3 = { + url: '/rest/process/key/TMMergeForVerKey-1-ja', + percentageComplete: 100, + statusCode: 'Cancelled' + } + expect(isProcessEnded(cancelledStatus3)).toEqual(true) + const notCancelled = { + url: '/rest/process/key/TMMergeForVerKey-1-ja', + // This should not affect the status code logic + percentageComplete: 9999, + statusCode: 'Running' + } + expect(isProcessEnded(notCancelled)).toEqual(false) + }) +}) diff --git a/server/zanata-frontend/src/frontend/app/components/SelectableDropdown/SelectableDropdown.test.js b/server/zanata-frontend/src/frontend/app/components/SelectableDropdown/SelectableDropdown.test.js new file mode 100644 index 0000000000..bc2a16cd71 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/SelectableDropdown/SelectableDropdown.test.js @@ -0,0 +1,38 @@ +jest.disableAutomock() + +import React from 'react' +import ReactDOMServer from 'react-dom/server' +import SelectableDropdown from '.' +import {MenuItem, DropdownButton} from 'react-bootstrap' + +const callback = () => {} + +describe('SelectableDropdown', () => { + it('can render SelectableDropdown markup', () => { + // Function used in TMMergeModal for a percentage selection dropdown + const valueToDisplay = v => `The function says ${v}` + const actual = ReactDOMServer.renderToStaticMarkup( + + ) + const expected = ReactDOMServer.renderToStaticMarkup( + + + The function says moo + + + The function says woof + + + The function says meow + + + ) + expect(actual).toEqual(expected) + }) +}) diff --git a/server/zanata-frontend/src/frontend/app/components/SelectableDropdown/index.js b/server/zanata-frontend/src/frontend/app/components/SelectableDropdown/index.js new file mode 100644 index 0000000000..47f0a26282 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/components/SelectableDropdown/index.js @@ -0,0 +1,85 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {MenuItem, DropdownButton} from 'react-bootstrap' +import {isEqual} from 'lodash' + +/** + * Selectable Dropdown + * + * TODO: pahuang document how this is different from a react bootstrap dropdown + */ +const SelectableDropdown = (props) => { + const { + id, + onSelectDropdownItem, + selectedValue, + title, + values, + valueToDisplay, + bsStyle, + bsSize + } = props + const items = values.map((v, index) => { + return ( + + ) + }) + const selection = selectedValue && valueToDisplay(selectedValue) + const titleValue = title || selection || '' + return ( + + {items} + + ) +} +SelectableDropdown.propTypes = { + id: PropTypes.string.isRequired, + onSelectDropdownItem: PropTypes.func.isRequired, + selectedValue: PropTypes.any, + values: PropTypes.arrayOf(PropTypes.any).isRequired, + // optinal function to convert value to display string + valueToDisplay: PropTypes.func.isRequired, + title: PropTypes.string, + bsStyle: PropTypes.string, + bsSize: PropTypes.string +} +SelectableDropdown.defaultProps = { + bsStyle: 'default', + bsSize: 'small', + valueToDisplay: v => v +} + +/** + * Sub-component of Dropdown menu item. + * Handles behavior of menu items + */ +class DropdownMenuItem extends Component { + static propTypes = { + value: PropTypes.any.isRequired, + onSelect: PropTypes.func.isRequired, + isSelected: PropTypes.bool.isRequired, + valueToDisplay: PropTypes.func + } + static defaultProps = { + valueToDisplay: v => v + } + onClick = () => { + this.props.onSelect(this.props.value) + } + render () { + const {value, isSelected, valueToDisplay} = this.props + const display = valueToDisplay(value) + return ( + + {display} + + ) + } +} + +export default SelectableDropdown diff --git a/server/zanata-frontend/src/frontend/app/components/Sidebar/index.jsx b/server/zanata-frontend/src/frontend/app/components/Sidebar/index.jsx index cea77f8c0c..9b08a96c73 100644 --- a/server/zanata-frontend/src/frontend/app/components/Sidebar/index.jsx +++ b/server/zanata-frontend/src/frontend/app/components/Sidebar/index.jsx @@ -17,8 +17,8 @@ class Sidebar extends Component { } toggleDisplay () { - this.setState({display: !this.state.display}) - this.setState({arrow: !this.state.arrow}) + this.setState(prevState => ({display: !prevState.display})) + this.setState(prevState => ({arrow: !prevState.arrow})) } /* eslint-disable react/jsx-no-bind, no-return-assign */ render () { diff --git a/server/zanata-frontend/src/frontend/app/components/index.js b/server/zanata-frontend/src/frontend/app/components/index.js index 6d850291f7..9d8b809f70 100644 --- a/server/zanata-frontend/src/frontend/app/components/index.js +++ b/server/zanata-frontend/src/frontend/app/components/index.js @@ -13,3 +13,6 @@ export LogoLoader from './LogoLoader' export Modal from './Modal' export Select from './Select' export Sidebar from './Sidebar' +export LockIcon from './LockIcon' +export DraggableVersionPanels from './DraggableVersionPanels' +export SelectableDropdown from './SelectableDropdown' diff --git a/server/zanata-frontend/src/frontend/app/containers/Glossary/NewEntryModal.js b/server/zanata-frontend/src/frontend/app/containers/Glossary/NewEntryModal.js index 3b7f72057b..7157a20727 100644 --- a/server/zanata-frontend/src/frontend/app/containers/Glossary/NewEntryModal.js +++ b/server/zanata-frontend/src/frontend/app/containers/Glossary/NewEntryModal.js @@ -8,6 +8,7 @@ import { glossaryToggleNewEntryModal, glossaryCreateNewEntry } from '../../actions/glossary-actions' +import update from 'immutability-helper' class NewEntryModal extends Component { static propTypes = { @@ -26,17 +27,13 @@ class NewEntryModal extends Component { } handleContentChanged = (e) => { - const { entry } = this.state - const { srcTerm } = entry - this.setState({ - entry: { - ...entry, - srcTerm: { - ...srcTerm, - content: e.target.value - } - } - }) + const content = e.target.value + this.setState(prevState => ({ + entry: update(prevState.entry, + {srcTerm: + {content: {$set: content}} + }) + })) } handlePosChanged = (e) => { diff --git a/server/zanata-frontend/src/frontend/app/containers/Languages/NewLanguageModal.js b/server/zanata-frontend/src/frontend/app/containers/Languages/NewLanguageModal.js index 8579431620..ccd51e0f08 100644 --- a/server/zanata-frontend/src/frontend/app/containers/Languages/NewLanguageModal.js +++ b/server/zanata-frontend/src/frontend/app/containers/Languages/NewLanguageModal.js @@ -67,21 +67,21 @@ class NewLanguageModal extends Component { } updateField = (field, e) => { - this.setState({ + this.setState(prevState => ({ details: { - ...this.state.details, + ...prevState.details, [field]: e.target.value } - }) + })) } updateCheckbox = (field) => { - this.setState({ + this.setState(prevState => ({ details: { - ...this.state.details, - [field]: !this.state.details[field] + ...prevState.details, + [field]: !prevState.details[field] } - }) + })) } validateDetails = () => { @@ -238,7 +238,7 @@ class NewLanguageModal extends Component { diff --git a/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/ProjectVersionOptions.js b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/ProjectVersionOptions.js new file mode 100644 index 0000000000..13b4fe0f92 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/ProjectVersionOptions.js @@ -0,0 +1,72 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { + Col, Panel, Checkbox, ListGroup, ListGroupItem, Label +} from 'react-bootstrap' + +const CopyLabel = (props) => { + return props.copy + ? () + : () +} +CopyLabel.propTypes = { + copy: PropTypes.bool.isRequired +} + +export const ProjectVersionOptions = (props) => { + const { + differentDocId, + differentContext, + fromImportedTM, + onDocIdCheckboxChange, + onContextCheckboxChange, + onImportedCheckboxChange + } = props + return ( + + + + + + Different DocID + Document name and path + + + + + + + + Different Context + resId, msgctxt + + + + + + + + + Match from Imported TM + + + + + + + ) +} +ProjectVersionOptions.propTypes = { + differentDocId: PropTypes.bool.isRequired, + differentContext: PropTypes.bool.isRequired, + fromImportedTM: PropTypes.bool.isRequired, + onDocIdCheckboxChange: PropTypes.func.isRequired, + onContextCheckboxChange: PropTypes.func.isRequired, + onImportedCheckboxChange: PropTypes.func.isRequired +} diff --git a/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/ProjectVersionPanels.js b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/ProjectVersionPanels.js new file mode 100644 index 0000000000..207b8e4df2 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/ProjectVersionPanels.js @@ -0,0 +1,163 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import { + Panel, Checkbox, ListGroup, ListGroupItem, PanelGroup +} from 'react-bootstrap' +import {LockIcon, Icon} from '../../components' +import {ProjectType, FromProjectVersionType, + versionDtoPropType} from '../../utils/prop-types-util' + +/** + * Panels for selecting and prioritising of project-versions + */ +class ProjectVersionPanels extends Component { + static propTypes = { + projectVersions: PropTypes.arrayOf(ProjectType).isRequired, + selectedVersions: PropTypes.arrayOf(FromProjectVersionType).isRequired, + /* params: version, projectSlug */ + onVersionCheckboxChange: PropTypes.func.isRequired, + /* params: project object */ + onAllVersionCheckboxChange: PropTypes.func.isRequired + } + /* + selectedVersions is an array of shape: + { + projectSlug, + version: { + id, + status + } + } + */ + selectedVersionsOfProject = (selectedVersions, project) => { + return selectedVersions + .filter(p => p.projectSlug === project.id) + .map(p => p.version) + } + render () { + if (this.props.projectVersions.length === 0) { + return + } + const panels = this.props.projectVersions.map((project, index) => { + const selectedVersionsInProject = + this.selectedVersionsOfProject(this.props.selectedVersions, project) + return ( + + ) + }) + return {panels} + } +} + +// util function to check if a version is in a list of versions by id comparison +const isVersionInList = + (versions, version) => !!versions.find(v => v.id === version.id) +/** + * Sub Component of a single project with versions. + * Handles behavior of display or selecting versions of this project. + */ +const SelectableProjectPanel = ({ + project, + selectedVersionsInProject, + onAllVersionCheckboxChange, + onVersionCheckboxChange }) => { + return ( + + + }> + + {project.versions.map((version, index) => { + const checked = isVersionInList(selectedVersionsInProject, version) + return ( + + + + ) + })} + + + ) +} +SelectableProjectPanel.propTypes = { + project: ProjectType.isRequired, + /* params: version, projectSlug */ + onVersionCheckboxChange: PropTypes.func.isRequired, + /* params: project object */ + onAllVersionCheckboxChange: PropTypes.func.isRequired, + selectedVersionsInProject: PropTypes.arrayOf(versionDtoPropType).isRequired +} + +/** + * Sub Component of project version panels + * Handles behavior of select all versions checkbox + */ +class SelectAllVersionsCheckbox extends Component { + static propTypes = { + project: ProjectType.isRequired, + selectedVersionsInProject: PropTypes.arrayOf(versionDtoPropType).isRequired, + onAllVersionCheckboxChange: PropTypes.func.isRequired + } + onAllVersionCheckboxChange = () => { + this.props.onAllVersionCheckboxChange(this.props.project) + } + render () { + const {project, selectedVersionsInProject} = this.props + // Check if all project versions have been selected + // since we are just comparing versions in one project, we can just check + // the size + const allVersionsChecked = + project.versions.length === selectedVersionsInProject.length + return ( + + + {project.title} + + ) + } +} + +/** + * Sub Component of project version panels + * Handles behavior of select version checkbox + */ +class VersionMenuCheckbox extends Component { + static propTypes = { + version: versionDtoPropType.isRequired, + onVersionCheckboxChange: PropTypes.func.isRequired, + projectSlug: PropTypes.string.isRequired, + checked: PropTypes.bool.isRequired + } + onVersionCheckboxChange = () => { + this.props.onVersionCheckboxChange( + this.props.version, this.props.projectSlug) + } + render () { + const { + version, + checked + } = this.props + return ( + + + {version.id} + + ) + } +} + +export default ProjectVersionPanels diff --git a/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/TMMergeModal.js b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/TMMergeModal.js new file mode 100644 index 0000000000..a1ccff2cf7 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/TMMergeModal.js @@ -0,0 +1,526 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import { differenceWith, isEqual, throttle } from 'lodash' +import {arrayMove} from 'react-sortable-hoc' +import {Button, Panel, Row, InputGroup, Col, FormControl} from 'react-bootstrap' +import { + Icon, Modal, LoaderText, SelectableDropdown, DraggableVersionPanels +} from '../../components' +import ProjectVersionPanels from './ProjectVersionPanels' +import {ProjectVersionVertical} from './project-version-displays' +import {ProjectVersionOptions} from './ProjectVersionOptions' +import CancellableProgressBar + from '../../components/ProgressBar/CancellableProgressBar' +import { + fetchVersionLocales, + fetchProjectPage, + toggleTMMergeModal, + mergeVersionFromTM, + queryTMMergeProgress, + cancelTMMergeRequest, + currentTMMergeProcessFinished +} from '../../actions/version-actions' +import { + ProjectType, LocaleType, FromProjectVersionType, processStatusType +} from '../../utils/prop-types-util.js' +import {isProcessEnded} from '../../utils/EnumValueUtils' +import {getVersionLanguageSettingsUrl} from '../../utils/UrlHelper' + +const percentValueToDisplay = v => `${v}%` +const localeToDisplay = l => l.displayName + +/* + * Component to display TM merge options + */ +const MergeOptions = ( + { + projectSlug, + versionSlug, + projectVersions, + locales, + fetchingProject, + fetchingLocale, + mergeOptions, + onContextCheckboxChange, + onDocIdCheckboxChange, + onImportedCheckboxChange, + onPercentSelection, + onLanguageSelection, + onProjectSearchChange, + flushProjectSearch, + onVersionCheckboxChange, + onAllVersionCheckboxChange, + onDragMoveEnd, + removeProjectVersion + }) => { + const noResults = (projectVersions.length === 0) ? 'No results' : '' + return ( +
    +

    + Copy existing translations from similar documents + in other projects and versions into this project version. +

    + + + TM match threshold + + + + + + + + + Language + + {fetchingLocale ? undefined : + + } + + + + + + +
    +
    + To + Target +
    + +
    +
    + + + + +
    + From + Source +
    + + + + + + + + + + + + Select source project versions to merge + +
    + + {noResults} +
    + + + + + +
    + +
    + ) +} +MergeOptions.propTypes = { + projectSlug: PropTypes.string.isRequired, + versionSlug: PropTypes.string.isRequired, + locales: PropTypes.arrayOf(LocaleType).isRequired, + projectVersions: PropTypes.arrayOf(ProjectType).isRequired, + fetchingProject: PropTypes.bool.isRequired, + fetchingLocale: PropTypes.bool.isRequired, + mergeOptions: PropTypes.shape({ + matchPercentage: PropTypes.number.isRequired, + differentDocId: PropTypes.bool.isRequired, + differentContext: PropTypes.bool.isRequired, + fromImportedTM: PropTypes.bool.isRequired, + selectedLanguage: LocaleType, + selectedVersions: PropTypes.arrayOf(FromProjectVersionType), + projectSearchTerm: PropTypes.string + }).isRequired, + onDocIdCheckboxChange: PropTypes.func.isRequired, + onContextCheckboxChange: PropTypes.func.isRequired, + onImportedCheckboxChange: PropTypes.func.isRequired, + onPercentSelection: PropTypes.func.isRequired, + onLanguageSelection: PropTypes.func.isRequired, + onProjectSearchChange: PropTypes.func.isRequired, + flushProjectSearch: PropTypes.func.isRequired, + onVersionCheckboxChange: PropTypes.func.isRequired, + onAllVersionCheckboxChange: PropTypes.func.isRequired, + onDragMoveEnd: PropTypes.func.isRequired, + removeProjectVersion: PropTypes.func.isRequired +} + +/** + * Root component for TM Merge Modal + */ +class TMMergeModal extends Component { + static propTypes = { + /* params: projectSlug and versionSlug */ + fetchVersionLocales: PropTypes.func.isRequired, + showTMMergeModal: PropTypes.bool.isRequired, + openTMMergeModal: PropTypes.func.isRequired, + /* params: project object */ + fetchProjectPage: PropTypes.func.isRequired, + projectSlug: PropTypes.string.isRequired, + versionSlug: PropTypes.string.isRequired, + locales: PropTypes.arrayOf(LocaleType).isRequired, + projectVersions: PropTypes.arrayOf(ProjectType).isRequired, + startMergeProcess: PropTypes.func.isRequired, + notification: PropTypes.object, + triggered: PropTypes.bool.isRequired, + fetchingProject: PropTypes.bool.isRequired, + fetchingLocale: PropTypes.bool.isRequired, + onCancelTMMerge: PropTypes.func.isRequired, + // Not required - set to undefined when merge not in progress + processStatus: processStatusType, + queryStatus: PropTypes.string, + queryTMMergeProgress: PropTypes.func.isRequired, + mergeProcessFinished: PropTypes.func.isRequired + } + defaultState = { + matchPercentage: 100, + differentDocId: false, + differentContext: false, + fromImportedTM: false, + selectedLanguage: undefined, + selectedVersions: [], + projectSearchTerm: this.props.projectSlug, + hasMerged: false + } + constructor (props) { + super(props) + this.state = this.defaultState + /* Chose 1 second as an arbitrary period between searches. + * leading and trailing options specify we want to search to after the user + * stops typing. */ + this.throttleHandleSearch = throttle(props.fetchProjectPage, 1000, + { 'leading': false }) + } + componentDidMount () { + this.props.fetchVersionLocales( + this.props.projectSlug, this.props.versionSlug) + this.props.fetchProjectPage(this.state.projectSearchTerm) + } + componentWillReceiveProps (nextProps) { + const { locales, showTMMergeModal } = nextProps + // Fetch locales again when modal is closed then re-opened + // Reset the state when the modal is closed + if (showTMMergeModal !== this.props.showTMMergeModal) { + if (showTMMergeModal) { + nextProps.fetchVersionLocales( + nextProps.projectSlug, nextProps.versionSlug) + } else { + // If a merge has run, reload the page to display the merge results + if (this.state.hasMerged) { + window.location.reload() + // Else reset the state to default on modal close + } else { + this.setState(this.defaultState) + } + } + } + if (!this.state.selectedLanguage) { + this.setState((prevState, props) => ({ + selectedLanguage: locales.length === 0 ? undefined : locales[0] + })) + } + const currentProcessStatus = this.props.processStatus + if (!isProcessEnded(currentProcessStatus) && + isProcessEnded(nextProps.processStatus)) { + this.setState({ + hasMerged: true + }) + // process just finished, we want to re-display the merge option form. + // but we want to delay it a bit so that the user can see the progress + // bar animation finishes + setTimeout(this.props.mergeProcessFinished, 1000) + } + // Filter out the source project and version if present in search results + // TODO: perform this filtering on the server side when retrieving projects + nextProps.projectVersions.map((project) => { + project.versions = project.versions.filter((version) => { + return project.id !== nextProps.projectSlug || + version.id !== nextProps.versionSlug + }) + }) + } + queryTMMergeProgress = () => { + this.props.queryTMMergeProgress(this.props.processStatus.url) + } + cancelTMMerge = () => { + this.props.onCancelTMMerge(this.props.processStatus.cancelUrl) + } + onPercentSelection = (percent) => { + this.setState({ + matchPercentage: percent + }) + } + onLanguageSelection = (language) => { + this.setState({ + selectedLanguage: language + }) + } + onProjectSearchChange = (event) => { + const textEntered = event.target.value + this.throttleHandleSearch(textEntered) + this.setState({ + projectSearchTerm: textEntered + }) + } + flushProjectSearch = (event) => { + if (event.key === 'Enter') { + this.throttleHandleSearch.flush() + } + } + // Sorts the selectedVersion list after a reorder of the Draggable List + onDragMoveEnd = ({oldIndex, newIndex}) => { + this.setState((prevState, props) => ({ + selectedVersions: + arrayMove(prevState.selectedVersions, oldIndex, newIndex) + })) + } + // Remove a version from fromProjectVersion array + removeProjectVersion = (project, version) => { + this.setState((prevState, props) => ({ + selectedVersions: prevState.selectedVersions.filter(({ projectSlug, + version: { id } }) => projectSlug !== project || id !== version.id)})) + } + // Remove all versions of a Project from fromProjectVersion array + removeAllProjectVersions = (projectSlug) => { + this.setState((prevState) => { + return { + selectedVersions: prevState.selectedVersions + .filter(p => projectSlug !== p.projectSlug) + } + }) + } + // Add a version to fromProjectVersion array + pushProjectVersion = (projectVersion) => { + this.setState((prevState, props) => ({ + selectedVersions: [...prevState.selectedVersions, projectVersion] + })) + } + // Add all versions of a Project to fromProjectVersion array + pushAllProjectVersions = (projectVersions) => { + this.setState(prevState => { + return { + selectedVersions: prevState.selectedVersions.concat(projectVersions) + } + }) + } + isProjectVersionSelected = (projectSlug, version) => { + return this.state.selectedVersions + .find(p => p.projectSlug === projectSlug && p.version.id === version.id) + } + // Remove/Add version from fromProjectVersion array based on selection + onVersionCheckboxChange = (version, projectSlug) => { + const versionChecked = this.isProjectVersionSelected(projectSlug, version) + versionChecked ? this.removeProjectVersion(projectSlug, version) + : this.pushProjectVersion({version, projectSlug: projectSlug}) + } + // Remove/Add all project versions to version list + onAllVersionCheckboxChange = (project) => { + const projectSlug = project.id + const versionsInProject = project.versions.map((version) => { + return {version, projectSlug} + }) + const diff = differenceWith(versionsInProject, + this.state.selectedVersions, isEqual) + if (diff.length === 0) { + // we already have all versions in this project selected, + // the operation is to remove them all + this.removeAllProjectVersions(projectSlug) + } else { + // we want to add all versions to the selection + this.pushAllProjectVersions(diff) + } + } + // Different DocID Checkbox handling + onDocIdCheckboxChange = () => { + this.setState((prevState, props) => ({ + differentDocId: !prevState.differentDocId + })) + } + // Different Context Checkbox handling + onContextCheckboxChange = () => { + this.setState((prevState, props) => ({ + differentContext: !prevState.differentContext + })) + } + // Match from Imported TM Checkbox handling + onImportedCheckboxChange = () => { + this.setState((prevState, props) => ({ + fromImportedTM: !prevState.fromImportedTM + })) + } + submitForm = () => { + this.props.startMergeProcess(this.props.projectSlug, + this.props.versionSlug, this.state) + } + render () { + const { + showTMMergeModal, + openTMMergeModal, + projectSlug, + versionSlug, + projectVersions, + locales, + notification, + triggered, + fetchingProject, + fetchingLocale, + processStatus + } = this.props + const noVersionsToMerge = (this.state.selectedVersions.length === 0) + const modalBody = processStatus + ? ( + + ) + : locales.length === 0 + ?

    This version has no enabled languages. You must enable at least one + language to use TM merge.
    + + Language Settings

    + : ( + + ) + const modalFooter = processStatus + ? undefined + : ( + + + + + + + ) + return ( + + + Version TM Merge +

    + {notification && notification.message}

    +
    + {modalBody} + {modalFooter} +
    + ) + } +} + +const mapStateToProps = (state) => { + const { + projectVersion: { + locales, + notification, + fetchingProject, + fetchingLocale, + TMMerge: { + show, + triggered, + projectVersions, + processStatus, + queryStatus + } + } + } = state + return { + showTMMergeModal: show, + triggered, + locales, + projectVersions, + notification, + fetchingProject, + fetchingLocale, + processStatus, + queryStatus + } +} + +const mapDispatchToProps = (dispatch) => { + return { + fetchVersionLocales: (project, version) => { + dispatch(fetchVersionLocales(project, version)) + }, + fetchProjectPage: (project) => { + dispatch(fetchProjectPage(project)) + }, + openTMMergeModal: () => { + dispatch(toggleTMMergeModal()) + }, + startMergeProcess: (projectSlug, versionSlug, mergeOptions) => { + dispatch(mergeVersionFromTM(projectSlug, versionSlug, mergeOptions)) + }, + queryTMMergeProgress: (url) => { + dispatch(queryTMMergeProgress(url)) + }, + onCancelTMMerge: (url) => { + dispatch(cancelTMMergeRequest(url)) + }, + mergeProcessFinished: () => { + dispatch(currentTMMergeProcessFinished()) + } + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(TMMergeModal) diff --git a/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/index.css b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/index.css new file mode 100644 index 0000000000..2ed2b04c00 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/index.css @@ -0,0 +1,90 @@ +/* Override legacy JSF new-zanata classes for TMMergeModal React components */ + +.panel-title { + font-size: 1.1rem !important; + margin: 0 !important; +} + +.react-draggable.list-group-item span.text-muted { + margin-left: 1em !important; +} + +.drag-handle { + top : 50% !important; + right: 3% !important; + -ms-transform: translate(3%, -50%); + transform : translate(3%, -50%); + padding: 0.75rem !important; + cursor: ns-resize !important; +} + +.rm-version-btn { + top : 50% !important; + right: 3% !important; + -ms-transform: translate(3%, -50%); + transform : translate(3%, -50%); + padding: 0.75rem !important; + color: red !important; +} + +.sortable-helper { + z-index: 10000 !important; +} + +.new-zanata .button.btn-primary { + color: #fff !important; + border-color: @color-neutral !important; + background-color: @color-light !important; +} + +.new-zanata .button.btn-link, .btn-link.active, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link { + background-color: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + border: none !important; +} + +.new-zanata #TM-merge-modal ul { + margin: 0; + padding: 0; +} + +.new-zanata #TM-merge-modal ul, .new-zanata #TM-merge-modal ul li { + list-style-type: none !important; +} + +.new-zanata #TM-merge-modal input[type=text] { + margin: 0; +} + +.new-zanata #TM-merge-modal label { + color: @color-dark !important; + font-weight: 500; +} + +#TM-merge-modal .progress-bar { + height: 100% !important; +} + +#TM-merge-modal .panel-group .panel-heading { + border-bottom: 1px solid #cdd4dc !important; +} + +#TM-merge-modal .modal-body .intro { + margin-bottom: 1rem !important; +} + +#TM-merge-modal .modal-header { + padding-bottom: 0 !important; +} + + +#TM-merge-modal .modal-body { + padding-top: 0 !important; +} + +#TM-merge-modal { + visibility: visible; + opacity: inherit; + backface-visibility: hidden; +} diff --git a/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/index.js b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/index.js new file mode 100644 index 0000000000..2cfbfb2d69 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/index.js @@ -0,0 +1,45 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import Helmet from 'react-helmet' +import TMMergeModal from './TMMergeModal' + +import { + toggleTMMergeModal +} from '../../actions/version-actions' + +/** + * Root component for Project Version Page + */ +class ProjectVersion extends Component { + static propTypes = { + openTMMergeModal: PropTypes.func.isRequired, + params: PropTypes.shape({ + project: PropTypes.string.isRequired, + version: PropTypes.string.isRequired + }) + } + + render () { + const { params } = this.props + return ( +
    + +
    + +
    +
    + ) + } +} + +const mapDispatchToProps = (dispatch) => { + return { + openTMMergeModal: () => { + dispatch(toggleTMMergeModal()) + } + } +} + +export default connect(undefined, mapDispatchToProps)(ProjectVersion) diff --git a/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/project-version-displays.js b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/project-version-displays.js new file mode 100644 index 0000000000..ea819e15a9 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/ProjectVersion/project-version-displays.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {Icon} from '../../components' + +export const ProjectVersionVertical = ({projectSlug, versionSlug}) => { + return ( +
      +
    • + + {projectSlug} +
    • +
    • + + {versionSlug} +
    • +
    + ) +} +ProjectVersionVertical.propTypes = { + projectSlug: PropTypes.string.isRequired, + versionSlug: PropTypes.string.isRequired +} diff --git a/server/zanata-frontend/src/frontend/app/containers/Root.js b/server/zanata-frontend/src/frontend/app/containers/Root.js index 361896e9a5..16de9714d6 100644 --- a/server/zanata-frontend/src/frontend/app/containers/Root.js +++ b/server/zanata-frontend/src/frontend/app/containers/Root.js @@ -5,6 +5,7 @@ import { Router, Route, Redirect } from 'react-router' import App from '../containers/App' import Glossary from '../containers/Glossary' import Languages from '../containers/Languages' +import ProjectVersion from '../containers/ProjectVersion' import TMX from '../containers/TMX' import Explore from '../containers/Explore' import UserProfile from '../containers/UserProfile' @@ -31,9 +32,11 @@ export default class Root extends Component { component={Glossary} /> + - - + diff --git a/server/zanata-frontend/src/frontend/app/containers/TMX/index.css b/server/zanata-frontend/src/frontend/app/containers/TMX/index.css new file mode 100644 index 0000000000..a7dfaa97cc --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/containers/TMX/index.css @@ -0,0 +1,14 @@ +#tmx-export-modal .modal-header { + padding-bottom: 0 !important; +} + + +#tmx-export-modal .modal-body { + padding-top: 0 !important; +} + +#tmx-export-modal { + visibility: visible; + opacity: inherit; + backface-visibility: hidden; +} diff --git a/server/zanata-frontend/src/frontend/app/containers/TMX/index.js b/server/zanata-frontend/src/frontend/app/containers/TMX/index.js index d4ff328792..580c4ba176 100644 --- a/server/zanata-frontend/src/frontend/app/containers/TMX/index.js +++ b/server/zanata-frontend/src/frontend/app/containers/TMX/index.js @@ -20,38 +20,26 @@ class TMXExportModal extends Component { show: PropTypes.bool, showSourceLanguages: PropTypes.bool, type: PropTypes.oneOf(TMX_TYPE).isRequired, - project: PropTypes.string, - version: PropTypes.string, srcLanguages: PropTypes.arrayOf(PropTypes.object), handleOnClose: PropTypes.func, handleToggleShowSourceLanguages: PropTypes.func, handleExportTMX: PropTypes.func, handleInitLoad: PropTypes.func.isRequired, - params: PropTypes.object + params: PropTypes.shape({ + project: PropTypes.string.isRequired, + version: PropTypes.string.isRequired + }) } componentDidMount () { - const project = this.props.params.project - const version = this.props.params.version - let type - if ((project || !isUndefined(project)) && - (!version || isUndefined(version))) { - type = TMX_TYPE[1] // project type - } else if ((project || !isUndefined(project)) && - (version || !isUndefined(version))) { - type = TMX_TYPE[2] // project version type - } else { - type = TMX_TYPE[0] // all type - } - this.props.handleInitLoad(type, project, version) + const { project, version } = this.props.params + this.props.handleInitLoad(project, version) } render () { const { show, showSourceLanguages, - project, - version, srcLanguages, handleOnClose, handleToggleShowSourceLanguages, @@ -59,6 +47,8 @@ class TMXExportModal extends Component { type } = this.props + const {project, version} = this.props.params + let title = '' let question = '' switch (type) { @@ -67,11 +57,11 @@ class TMXExportModal extends Component { question = 'Are you sure you want to all projects to TMX?' break case 'project': - title = 'Export project ' + project + ' to TMX' + title = 'Export project \'' + project + '\' to TMX' question = 'Are you sure you want to export this project to TMX?' break case 'version': - title = 'Export version ' + version + ' to TMX' + title = 'Export version \'' + version + '\' to TMX' question = 'Are you sure you want to this version to TMX?' break default: @@ -110,7 +100,7 @@ class TMXExportModal extends Component { * @@ -120,42 +110,45 @@ class TMXExportModal extends Component { }) } return ( - - - {title} - - - {showSourceLanguages && !isEmpty(srcLanguagesRow) - ? -

    Source languages

    - - - {srcLanguagesRow} - -
    -
    - : -

    {question}
    - Default source language - {isEmpty(srcLanguagesRow) - ? en-US - : en-US - } - -

    -
    -

    - -

    -
    - } - -
    -
    +
    +
    + + + {title} + + + {showSourceLanguages && !isEmpty(srcLanguagesRow) + ? +

    Source languages

    + + + {srcLanguagesRow} + +
    +
    + : +

    {question}
    + Default source language + {isEmpty(srcLanguagesRow) + ? en-US + : en-US + } + +

    +
    +

    + +

    +
    + } +
    +
    +
    +
    ) } } @@ -166,9 +159,7 @@ const mapStateToProps = (state) => { show: tmxExport.showModal, srcLanguages: tmxExport.sourceLanguages, showSourceLanguages: tmxExport.showSourceLanguages, - type: tmxExport.type, - project: tmxExport.project, - version: tmxExport.version + type: tmxExport.type } } @@ -181,11 +172,12 @@ const mapDispatchToProps = (dispatch) => { handleToggleShowSourceLanguages: () => { dispatch(toggleShowSourceLanguages()) }, - handleExportTMX: () => { - dispatch(exportTMX()) + handleExportTMX: (localeId) => { + dispatch(exportTMX(localeId)) + dispatch(showExportTMXModal(false)) }, - handleInitLoad: (type, project, version) => { - dispatch(tmxInitialLoad(type, project, version)) + handleInitLoad: (project, version) => { + dispatch(tmxInitialLoad(project, version)) } } } diff --git a/server/zanata-frontend/src/frontend/app/containers/UserProfile/RecentContributions.jsx b/server/zanata-frontend/src/frontend/app/containers/UserProfile/RecentContributions.jsx index a951c10219..d08949e6f4 100644 --- a/server/zanata-frontend/src/frontend/app/containers/UserProfile/RecentContributions.jsx +++ b/server/zanata-frontend/src/frontend/app/containers/UserProfile/RecentContributions.jsx @@ -34,9 +34,9 @@ class RecentContributions extends React.Component { } onToggleShowDateRange = () => { - this.setState({ - showDateRange: !this.state.showDateRange - }) + this.setState(prevState => ({ + showDateRange: !prevState.showDateRange + })) } onDateRangeChanged = (dateRange) => { diff --git a/server/zanata-frontend/src/frontend/app/editor/components/SettingOption/SettingOption.story.js b/server/zanata-frontend/src/frontend/app/editor/components/SettingOption/SettingOption.story.js new file mode 100644 index 0000000000..7eff456a8b --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/editor/components/SettingOption/SettingOption.story.js @@ -0,0 +1,41 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { storiesOf, action } from '@kadira/storybook' +import RealSettingOption from '.' + +class SettingOption extends React.Component { + static propTypes = { + id: PropTypes.any.isRequired, + label: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired, + updateSetting: PropTypes.func.isRequired + } + constructor (props) { + super(props) + this.state = {active:props.active} + } + updateSetting = (id, active) => { + // record the check state in the wrapper + this.setState({ active:active }) + // call the real one that was passed in + this.props.updateSetting( id, active) + } + render () { + return ( + + ) + } +} + +storiesOf('SettingOption', module) + .add('default', () => ( + + )) diff --git a/server/zanata-frontend/src/frontend/app/editor/components/SettingOption/index.js b/server/zanata-frontend/src/frontend/app/editor/components/SettingOption/index.js new file mode 100644 index 0000000000..8c8a090c7f --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/editor/components/SettingOption/index.js @@ -0,0 +1,31 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Checkbox } from 'react-bootstrap' + +class SettingOption extends React.Component { + propTypes = { + setting: PropTypes.shape({ + id: PropTypes.any.isRequired, // I will update this to whatever I use when I wire it up + label: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired + }).isRequired, + /* arguments: (any: settingId, bool: active) */ + updateSetting: PropTypes.func.isRequired + } + + updateSetting = (event) => { + this.props.updateSetting(this.props.id, event.target.checked) + } + + render () { + const { label, active } = this.props + return ( + +  {label} + + ) + } +} + +export default SettingOption diff --git a/server/zanata-frontend/src/frontend/app/editor/components/SettingsOptions/SettingsOptions.story.js b/server/zanata-frontend/src/frontend/app/editor/components/SettingsOptions/SettingsOptions.story.js new file mode 100644 index 0000000000..9104422f44 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/editor/components/SettingsOptions/SettingsOptions.story.js @@ -0,0 +1,219 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { storiesOf, action } from '@kadira/storybook' +import RealSettingsOptions from '.' + +class SettingsOptions extends React.Component { + static propTypes = { + settings: PropTypes.shape({ + id: PropTypes.any.isRequired, + label: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired + }).isRequired, + updateSetting: PropTypes.func.isRequired + } + constructor (props) { + super(props) + this.state = { settings: props.settings } + } + updateSetting = (id, active) => { + // record the check state in the wrapper + this.setState(newState => ({ + settings: newState.settings.map(setting => { + if (setting.id === id) { + return { + ...setting, + active + } + } else { + return setting + } + }) + })) + // call the real one that was passed in + this.props.updateSetting(id, active) + } + render () { + return ( + + ) + } +} + +const updateSetting = action('updateSetting') +const listUnchecked = + [ + { + id: 'list-item-1', + label: 'List item 1', + active: false + }, + { + id: 'list-item-2', + label: 'List item 2', + active: false + }, + { + id: 'list-item-3', + label: 'List item 3', + active: false + }, + { + id: 'list-item-4', + label: 'List item 4', + active: false + } + ] +const listHalfChecked = + [ + { + id: 'list-item-1', + label: 'List item 1', + active: false + }, + { + id: 'list-item-2', + label: 'List item 2', + active: true + }, + { + id: 'list-item-3', + label: 'List item 3', + active: false + }, + { + id: 'list-item-4', + label: 'List item 4', + active: true + } + ] +const listAllChecked = + [ + { + id: 'list-item-1', + label: 'List item 1', + active: true + }, + { + id: 'list-item-2', + label: 'List item 2', + active: true + }, + { + id: 'list-item-3', + label: 'List item 3', + active: true + }, + { + id: 'list-item-4', + label: 'List item 4', + active: true + } + ] + +const settings = + [ + { + id: 'key-saves', + label: 'Enter key saves immediately', + active: true + }, + { + id: 'syntax-highlight', + label: 'Syntax highlighting', + active: false + } + ] + +const defaults = + [ + { + id: 'suggestions-diff', + label: 'Suggestions diff', + active: false + }, + { + id: 'panel-layout', + label: 'Panel layout', + active: false + } + ] + +const validations = + [ + { + id: 'html-xml-tags', + label: 'HTML/XML tags', + active: true + }, + { + id: 'java-variables', + label: 'Java variables', + active: true + }, + { + id: 'leading-trailing-newline', + label: 'Leading/trailing newline', + active: true + }, + { + id: 'positional-printf', + label: 'Positional printf (XSI extension)', + active: false + }, + { + id: 'printf-variables', + label: 'Printf variables', + active: false + }, + { + id: 'tab-characters', + label: 'Tab characters', + active: true + }, + { + id: 'xml-entity-reference', + label: 'XML entity reference', + active: false + } + ] + +storiesOf('SettingsOptions', module) + .add('default - unchecked', () => ( + + )) + .add('default - half-checked', () => ( + + )) + .add('default - all checked', () => ( + + )) + .add('EDITOR OPTIONS', () => ( +
    +

    Editor options

    + +

    + Set current layouts as default:

    + +
    + )) + .add('VALIDATION SETTINGS', () => ( +
    +

    Validation settings

    + +
    + )) diff --git a/server/zanata-frontend/src/frontend/app/editor/components/SettingsOptions/index.js b/server/zanata-frontend/src/frontend/app/editor/components/SettingsOptions/index.js new file mode 100644 index 0000000000..06d4e8e1e4 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/editor/components/SettingsOptions/index.js @@ -0,0 +1,33 @@ +import React from 'react' +import PropTypes from 'prop-types' +import SettingOption from '../SettingOption' + +const SettingsOptions = ({settings, updateSetting}) => { + + const checkboxes = settings.map((setting, index) => ( +
  • + +
  • + )) + return ( +
    +
      + {checkboxes} +
    +
    + ) +} + +SettingsOptions.propTypes = { + settings: PropTypes.shape({ + id: PropTypes.any.isRequired, // I will update this to whatever I use when I wire it up + label: PropTypes.string.isRequired, + active: PropTypes.bool.isRequired + }).isRequired, + /* arguments: (any: settingId, bool: active) */ + updateSetting: PropTypes.func.isRequired +} + +export default SettingsOptions diff --git a/server/zanata-frontend/src/frontend/app/editor/components/ValidationOptions/ValidationOptions.story.js b/server/zanata-frontend/src/frontend/app/editor/components/ValidationOptions/ValidationOptions.story.js deleted file mode 100644 index 308ad64a45..0000000000 --- a/server/zanata-frontend/src/frontend/app/editor/components/ValidationOptions/ValidationOptions.story.js +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { storiesOf, action } from '@kadira/storybook' -import RealValidationOptions from '.' - -/* Wrapper class for storybook. - - * The checkbox states will be stored and updated in the live - * app. This wrapper stores the states so that we can see - * them working in storybook too. - */ -class ValidationOptions extends React.Component { - static propTypes = { - states: PropTypes.object.isRequired, - updateValidationOption: PropTypes.func.isRequired - } - constructor (props) { - super(props) - this.state = props.states - } - updateValidationOption = (validation, checked) => { - // record the check state in the wrapper - this.setState({ [validation]: checked }) - // call the real one that was passed in - this.props.updateValidationOption(validation, checked) - } - render () { - return ( - - ) - } -} - -const updateAction = action('updateValidationOption') -/* - * See .storybook/README.md for info on the component storybook. - */ -storiesOf('ValidationOptions', module) - .add('default', () => ( - - )) - - .add('half checked', () => ( - - )) - - .add('all checked', () => ( - - )) diff --git a/server/zanata-frontend/src/frontend/app/editor/components/ValidationOptions/index.js b/server/zanata-frontend/src/frontend/app/editor/components/ValidationOptions/index.js deleted file mode 100644 index d227c56ae5..0000000000 --- a/server/zanata-frontend/src/frontend/app/editor/components/ValidationOptions/index.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Checkbox } from 'react-bootstrap' - -const validations = - ['HTML/XML tags', - 'Java variables', - 'Leading/trailing newline (n)', - 'Positional printf (XSI extension)', - 'Printf variables', - 'Tab characters (t)', - 'XML entity reference'] - -const ValidationOptions = ({states, updateValidationOption}) => { - const checkboxes = validations.map((validation, index) => ( -
  • - -
  • - )) - return ( -
    -

    Validation options

    -
      - {checkboxes} -
    -
    - ) -} - -ValidationOptions.propTypes = { - states: PropTypes.shape({ - 'HTML/XML tags': PropTypes.bool.isRequired, - 'Java variables': PropTypes.bool.isRequired, - 'Leading/trailing newline (n)': PropTypes.bool.isRequired, - 'Positional printf (XSI extension)': PropTypes.bool.isRequired, - 'Printf variables': PropTypes.bool.isRequired, - 'Tab characters (t)': PropTypes.bool.isRequired, - 'XML entity reference': PropTypes.bool.isRequired - }).isRequired, - updateValidationOption: PropTypes.func.isRequired -} - -class ValidationCheckbox extends React.Component { - static propTypes = { - validation: PropTypes.string.isRequired, - checked: PropTypes.bool.isRequired, - /* Will be called with (validation, newValue) */ - onChange: PropTypes.func.isRequired - } - - onChange = (event) => { - this.props.onChange(this.props.validation, event.target.checked) - } - - render () { - const { validation, checked } = this.props - return ( - - {validation} - - ) - } -} - -export default ValidationOptions diff --git a/server/zanata-frontend/src/frontend/app/editor/components/components.story.js b/server/zanata-frontend/src/frontend/app/editor/components/components.story.js index c316cb43de..746aae6851 100644 --- a/server/zanata-frontend/src/frontend/app/editor/components/components.story.js +++ b/server/zanata-frontend/src/frontend/app/editor/components/components.story.js @@ -11,4 +11,5 @@ require('./ProgressBar/ProgressBar.story.js') require('./GlossarySearchInput/GlossarySearchInput.story.js') require('./GlossaryTerm/GlossaryTerm.story.js') require('./GlossaryTermModal/GlossaryTermModal.story.js') -require('./ValidationOptions/ValidationOptions.story') +require('./SettingOption/SettingOption.story.js') +require('./SettingsOptions/SettingsOptions.story.js') diff --git a/server/zanata-frontend/src/frontend/app/editor/containers/Root/index.css b/server/zanata-frontend/src/frontend/app/editor/containers/Root/index.css index f8e55c45d8..2b97fe3842 100644 --- a/server/zanata-frontend/src/frontend/app/editor/containers/Root/index.css +++ b/server/zanata-frontend/src/frontend/app/editor/containers/Root/index.css @@ -197,8 +197,9 @@ font-weight: 600; } -li.docName { - max-width: 20em; +li.docName .row { + display: flex; + width: 12rem; } li.docName span.ellipsis { @@ -855,6 +856,11 @@ label span.n1 { max-width: 9.5em; } + li.docName .row { + display: flex; + width: 20rem; + } + #sidebartabs-pane-1 table tbody tr { display: table-row !important; } diff --git a/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.css b/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.css index b51a623a5d..65c7a322db 100644 --- a/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.css +++ b/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.css @@ -402,6 +402,27 @@ select.settings-select { color: var(--Sidebar-color-text); } +.settings-heading { + font-weight: 500; +} + +h2.settings-heading { + padding-bottom:1rem; +} + +h3.settings-heading { + font-size: 0.875rem; + padding-bottom: 0.375rem; + padding-top: 0.75rem; +} + +.settings-options p { + font-weight: 500; + color: var(--Sidebar-color-text); + font-size: 0.875rem; +} + + /* --------------------- */ diff --git a/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.js b/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.js index 0715580698..7849a0c477 100644 --- a/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.js +++ b/server/zanata-frontend/src/frontend/app/editor/containers/Sidebar/index.js @@ -38,7 +38,7 @@ class Sidebar extends Component { // } // // mediaQueryChanged () { - // this.setState({docked: this.state.mql.matches}) + // this.setState(prevState => ({docked: prevState.mql.matches})) // } setOpen = (open) => { diff --git a/server/zanata-frontend/src/frontend/app/editor/containers/SuggestionDetailsModal/index.css b/server/zanata-frontend/src/frontend/app/editor/containers/SuggestionDetailsModal/index.css index b0afee3e29..29fa96a183 100644 --- a/server/zanata-frontend/src/frontend/app/editor/containers/SuggestionDetailsModal/index.css +++ b/server/zanata-frontend/src/frontend/app/editor/containers/SuggestionDetailsModal/index.css @@ -8,12 +8,12 @@ .Editor-suggestionsBody .TransUnit--suggestion .TransUnit-text--tight { padding-top: 0 !important; - font-size:1em; + font-size: 1em; } .TransUnit--suggestion .TransUnit-text--tight { - padding-top:40px !important; - font-size:1.2em; + padding-top: 40px !important; + font-size: 1.2em; } /* Based on bootstrap styles. */ @@ -68,7 +68,7 @@ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); box-shadow: 0 1px 1px rgba(0, 0, 0, .05); background-color: #fcfcfc; - line-height:1.2em; + line-height: 1.2em; } .panel-body { @@ -86,7 +86,7 @@ margin-bottom: 0; color: inherit; font-size: 1.143em; - padding-bottom:0 + padding-bottom: 0; } .panel.panel-side { @@ -113,7 +113,7 @@ .panel-info > .panel-heading { border-bottom: solid 1px var(--brand-info); color: var(--brand-info); - background-color:#fcfcfc; + background-color: #fcfcfc; font-weight: 300; } @@ -193,14 +193,14 @@ } .TransUnit-details { - white-space:normal; + white-space: normal; word-break: break-all; - line-height:1em; + line-height: 1em; } .TransUnit-details .u-listInline { - display:inline-flex; - width:100%; + display: block; + width: 100%; } li.TransUnit-label-suggestions { @@ -208,9 +208,9 @@ li.TransUnit-label-suggestions { } span.TransUnit-details-inner { - padding-left:0.375em; - font-weight:500; - color:#444c54; + padding-left: 0.375em; + font-weight: 500; + color: #444c54; } diff --git a/server/zanata-frontend/src/frontend/app/jsf/JsfRoot.js b/server/zanata-frontend/src/frontend/app/jsf/JsfRoot.js new file mode 100644 index 0000000000..1c327bf5cf --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/jsf/JsfRoot.js @@ -0,0 +1,33 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Provider } from 'react-redux' +import { Router, Route } from 'react-router' +import ProjectVersion from '../containers/ProjectVersion' +import TMX from '../containers/TMX' + +const App = ({children}) => { + return (
    {children}
    ) +} +App.propTypes = { + children: PropTypes.element +} + +const JsfRoot = ({store, history}) => { + return ( + + + + + + + + + ) +} +JsfRoot.propTypes = { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +} + +export default JsfRoot diff --git a/server/zanata-frontend/src/frontend/app/jsf/index.js b/server/zanata-frontend/src/frontend/app/jsf/index.js new file mode 100644 index 0000000000..e3c31936d0 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/jsf/index.js @@ -0,0 +1,58 @@ +/** + * This is to add React components to existing jsf page besides sidebar/legacy. + * Created by pahuang on 6/23/17. + */ +import React from 'react' +import { render } from 'react-dom' +import { createStore, applyMiddleware, compose } from 'redux' +import thunk from 'redux-thunk' +import createLogger from 'redux-logger' +import { history } from '../history' +import { syncHistoryWithStore } from 'react-router-redux' +import { apiMiddleware } from 'redux-api-middleware' +import JsfRoot from './JsfRoot' +import rootReducer from '../reducers' +import { + toggleTMMergeModal +} from '../actions/version-actions' +import { + showExportTMXModal +} from '../actions/tmx-actions' + +const logger = createLogger({ + predicate: (getState, action) => + process.env && (process.env.NODE_ENV === 'development') +}) + +const finalCreateStore = compose( + applyMiddleware( + thunk, + apiMiddleware, + // routerMiddleware, + logger + ) +)(createStore) + +// Call and assign the store with no initial state +const store = ((initialState) => { + const store = finalCreateStore(rootReducer, initialState) + if (module.hot) { + // Enable Webpack hot module replacement for reducers + module.hot.accept('../reducers', () => { + const nextRootReducer = require('../reducers') + store.replaceReducer(nextRootReducer) + }) + } + return store +})() + +const enhancedHistory = syncHistoryWithStore(history, store) +export default function mountReactComponent () { + // Attaching to window object so modal can be triggered from the JSF page + window.openTMMergeModal = () => store.dispatch(toggleTMMergeModal()) + window.openProjectTMXExportModal = (show) => + store.dispatch(showExportTMXModal(show)) + const mountPoint = document.getElementById('jsfReactRoot') + + render(, mountPoint) +} diff --git a/server/zanata-frontend/src/frontend/app/legacy.js b/server/zanata-frontend/src/frontend/app/legacy.js index 87432ad5a1..93a482e2d4 100644 --- a/server/zanata-frontend/src/frontend/app/legacy.js +++ b/server/zanata-frontend/src/frontend/app/legacy.js @@ -58,3 +58,6 @@ render( , document.getElementById('root') ) + +import mountReactToJsf from './jsf' +mountReactToJsf() diff --git a/server/zanata-frontend/src/frontend/app/reducers/index.js b/server/zanata-frontend/src/frontend/app/reducers/index.js index ff9a7ff8c0..25bb0eb24b 100644 --- a/server/zanata-frontend/src/frontend/app/reducers/index.js +++ b/server/zanata-frontend/src/frontend/app/reducers/index.js @@ -5,6 +5,7 @@ import explore from './explore-reducer' import profile from './profile-reducer' import common from './common-reducer' import languages from './languages-reducer' +import projectVersion from './version-reducer' import tmx from './tmx-reducer' const rootReducer = combineReducers({ @@ -14,6 +15,7 @@ const rootReducer = combineReducers({ common, profile, languages, + projectVersion, tmx }) diff --git a/server/zanata-frontend/src/frontend/app/reducers/tmx-reducer.js b/server/zanata-frontend/src/frontend/app/reducers/tmx-reducer.js index ce6bf3a775..472e537be5 100644 --- a/server/zanata-frontend/src/frontend/app/reducers/tmx-reducer.js +++ b/server/zanata-frontend/src/frontend/app/reducers/tmx-reducer.js @@ -74,7 +74,7 @@ const tmx = handleActions( tmxExport: { sourceLocale: undefined, targetLocale: undefined, - showModal: true, + showModal: false, showSourceLanguages: false, sourceLanguages: undefined, type: TMX_TYPE[0] diff --git a/server/zanata-frontend/src/frontend/app/reducers/version-reducer.js b/server/zanata-frontend/src/frontend/app/reducers/version-reducer.js new file mode 100644 index 0000000000..90aaac6311 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/reducers/version-reducer.js @@ -0,0 +1,142 @@ +import {handleActions} from 'redux-actions' +import update from 'immutability-helper' +import { + TOGGLE_TM_MERGE_MODAL, + VERSION_LOCALES_REQUEST, + VERSION_LOCALES_SUCCESS, + VERSION_LOCALES_FAILURE, + PROJECT_PAGE_REQUEST, + PROJECT_PAGE_SUCCESS, + PROJECT_PAGE_FAILURE, + VERSION_TM_MERGE_REQUEST, + VERSION_TM_MERGE_SUCCESS, + VERSION_TM_MERGE_FAILURE, + QUERY_TM_MERGE_PROGRESS_SUCCESS, + QUERY_TM_MERGE_PROGRESS_FAILURE, + TM_MERGE_PROCESS_FINISHED +} from '../actions/version-action-types' + +const defaultState = { + TMMerge: { + show: false, + triggered: false, + processStatus: undefined, + queryStatus: undefined, + projectVersions: [] + }, + // this works unless the code is sent back in time to the 1960s or earlier. + projectResultsTimestamp: new Date(0), + locales: [], + fetchingProject: false, + fetchingLocale: false, + notification: undefined +} + +const version = handleActions({ + [TOGGLE_TM_MERGE_MODAL]: (state, action) => { + return update(state, { + TMMerge: { show: { $set: !state.TMMerge.show } } + }) + }, + [VERSION_LOCALES_REQUEST]: (state, action) => { + return action.error ? update(state, { + fetchingLocale: { $set: false }, + notification: { $set: { + message: 'We were unable load locale information. ' + + 'Please refresh this page and try again.' + }} + }) : update(state, { + fetchingLocale: { $set: true }, + notification: { $set: undefined } + }) + }, + [VERSION_LOCALES_SUCCESS]: (state, action) => { + return update(state, { + locales: { $set: action.payload }, + fetchingLocale: { $set: false }, + notification: { $set: undefined } + }) + }, + [VERSION_LOCALES_FAILURE]: (state, action) => { + return update(state, { + fetchingLocale: { $set: false }, + notification: { $set: { + message: 'We were unable load locale information. ' + + 'Please refresh this page and try again.' + }} + }) + }, + [PROJECT_PAGE_REQUEST]: (state, action) => { + return action.error ? update(state, { + fetchingProject: { $set: false }, + notification: { $set: { + message: 'We were unable load project information. ' + + 'Please refresh this page and try again.' + }} + }) : update(state, { + fetchingProject: { $set: true }, + notification: { $set: undefined } + }) + }, + [PROJECT_PAGE_SUCCESS]: (state, action) => { + if (action.meta.timestamp > state.projectResultsTimestamp) { + return update(state, { + TMMerge: { projectVersions: { $set: action.payload } }, + fetchingProject: { $set: false }, + notification: { $set: undefined }, + projectResultsTimestamp: {$set: action.meta.timestamp} + }) + } else { + return state + } + }, + [PROJECT_PAGE_FAILURE]: (state, action) => { + return update(state, { + fetchingProject: { $set: false }, + notification: { $set: { + message: 'We were unable load project information. ' + + 'Please refresh this page and try again.' + }} + }) + }, + [VERSION_TM_MERGE_REQUEST]: (state, action) => { + return update(state, { + TMMerge: { triggered: { $set: true } }, + notification: { $set: undefined } + }) + }, + [VERSION_TM_MERGE_SUCCESS]: (state, action) => { + return update(state, { + TMMerge: + { processStatus: { $set: action.payload }, triggered: { $set: false } } + }) + }, + [VERSION_TM_MERGE_FAILURE]: (state, action) => { + return update(state, { + TMMerge: { triggered: { $set: false } }, + notification: { $set: { + message: + 'We were unable perform the operation. Please try again.' + } } + }) + }, + [QUERY_TM_MERGE_PROGRESS_SUCCESS]: (state, action) => { + return update(state, { + // Using merge to ensure cancelUrl is not lost + TMMerge: { processStatus: { $merge: action.payload } } + }) + }, + [QUERY_TM_MERGE_PROGRESS_FAILURE]: (state, action) => { + // what do we do with failed status query? + return update(state, { + TMMerge: { queryStatus: { $set: action.error } } + }) + }, + [TM_MERGE_PROCESS_FINISHED]: (state, action) => { + return update(state, { + TMMerge: { processStatus: { $set: undefined } } + }) + }}, + defaultState) + +export default version diff --git a/server/zanata-frontend/src/frontend/app/reducers/version-reducer.test.js b/server/zanata-frontend/src/frontend/app/reducers/version-reducer.test.js new file mode 100644 index 0000000000..4022f82ff4 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/reducers/version-reducer.test.js @@ -0,0 +1,338 @@ +/* global jest describe it expect */ +jest.disableAutomock() + +import versionReducer from './version-reducer' + +import { + TOGGLE_TM_MERGE_MODAL, + VERSION_LOCALES_REQUEST, + VERSION_LOCALES_SUCCESS, + VERSION_LOCALES_FAILURE, + PROJECT_PAGE_REQUEST, + PROJECT_PAGE_SUCCESS, + PROJECT_PAGE_FAILURE, + VERSION_TM_MERGE_REQUEST, + VERSION_TM_MERGE_SUCCESS, + VERSION_TM_MERGE_FAILURE, + TM_MERGE_CANCEL_REQUEST, + TM_MERGE_CANCEL_SUCCESS, + QUERY_TM_MERGE_PROGRESS_REQUEST, + QUERY_TM_MERGE_PROGRESS_FAILURE, + TM_MERGE_PROCESS_FINISHED +} from '../actions/version-action-types' + +describe('version-reducer test', () => { + it('can toggle the merge modal', () => { + const initial = versionReducer(undefined, { type: 'any' }) + const shown = versionReducer(initial, { + type: TOGGLE_TM_MERGE_MODAL, + payload: { show: true } + }) + const hidden = versionReducer(shown, { + type: TOGGLE_TM_MERGE_MODAL, + payload: { show: false } + }) + expect(initial.TMMerge).toEqual( + {processStatus: undefined, queryStatus: undefined, + projectVersions: [], show: false, triggered: false} + ) + expect(shown.TMMerge).toEqual( + {processStatus: undefined, queryStatus: undefined, + projectVersions: [], show: true, triggered: false} + ) + expect(hidden.TMMerge).toEqual( + {processStatus: undefined, queryStatus: undefined, + projectVersions: [], show: false, triggered: false} + ) + }) + + it('can track fetching locales', () => { + const initial = versionReducer(undefined, { type: 'any' }) + const requested = versionReducer(initial, { + type: VERSION_LOCALES_REQUEST + }) + expect(requested.fetchingLocale).toEqual(true) + const failed = versionReducer(requested, { + type: VERSION_LOCALES_FAILURE + }) + expect(failed.fetchingLocale).toEqual(false) + }) + + it('can receive locales', () => { + const requestAction = { + type: VERSION_LOCALES_REQUEST + } + const localeSuccessAction = { + type: VERSION_LOCALES_SUCCESS, + payload: + [{ + displayName: 'Japanese', + localeId: 'ja', + nativeName: '日本語' + }, + { + displayName: 'Azerbaijani', + localeId: 'az', + nativeName: 'azərbaycan dili' + }] + } + const initial = versionReducer(undefined, {type: 'any'}) + const localesRequested = versionReducer(initial, requestAction) + const localesReceived = versionReducer(localesRequested, localeSuccessAction) + + expect(localesRequested.fetchingLocale).toEqual(true) + expect(localesReceived.fetchingLocale).toEqual(false) + expect(localesReceived.locales).toEqual([{ + displayName: 'Japanese', + localeId: 'ja', + nativeName: '日本語' + }, + { + displayName: 'Azerbaijani', + localeId: 'az', + nativeName: 'azərbaycan dili' + }]) + }) + + it('can track fetching projects', () => { + const initial = versionReducer(undefined, { type: 'any' }) + const requested = versionReducer(initial, { + type: PROJECT_PAGE_REQUEST + }) + expect(requested.fetchingProject).toEqual(true) + const failed = versionReducer(requested, { + type: PROJECT_PAGE_FAILURE + }) + expect(failed.fetchingProject).toEqual(false) + }) + + it('can receive projects', () => { + const timestamp = Date.now() + const requestAction = { + type: PROJECT_PAGE_REQUEST + } + const projectSuccessAction = { + type: PROJECT_PAGE_SUCCESS, + meta: {timestamp}, + payload: + [{ + contributorCount: 0, + description: 'A project', + id: 'meikai', + status: 'ACTIVE', + title: 'Meikai', + type: 'Project', + versions: [ + { + id: 'ver1', + status: 'ACTIVE' + } + ] + }, + { + contributorCount: 0, + description: 'Locked project', + id: 'meikailocked', + status: 'READONLY', + title: 'MeikaiLocked', + type: 'Project', + versions: [ + { + id: 'ver1', + status: 'READONLY' + } + ] + }] + } + const initial = versionReducer(undefined, {type: 'any'}) + const projectsRequested = versionReducer(initial, requestAction) + const projectsReceived = versionReducer(projectsRequested, + projectSuccessAction) + expect(projectsRequested.fetchingProject).toEqual(true) + expect(projectsReceived.fetchingProject).toEqual(false) + expect(projectsReceived.TMMerge.projectVersions).toEqual( + [{ + contributorCount: 0, + description: 'A project', + id: 'meikai', + status: 'ACTIVE', + title: 'Meikai', + type: 'Project', + versions: [ + { + id: 'ver1', + status: 'ACTIVE' + } + ] + }, + { + contributorCount: 0, + description: 'Locked project', + id: 'meikailocked', + status: 'READONLY', + title: 'MeikaiLocked', + type: 'Project', + versions: [ + { + id: 'ver1', + status: 'READONLY' + } + ] + }] + ) + }) + + it('does not use stale results', () => { + const stalePayload = [{ + contributorCount: 0, + description: 'A stale project', + id: 'meikai1', + status: 'ACTIVE', + title: 'Meikai1', + type: 'Project', + versions: [ + { + id: 'ver1', + status: 'ACTIVE' + } + ] + }] + const freshPayload = [{ + contributorCount: 0, + description: 'A fresh project', + id: 'meikai2', + status: 'ACTIVE', + title: 'Meikai2', + type: 'Project', + versions: [ + { + id: 'ver1', + status: 'ACTIVE' + } + ] + }] + const brunch = new Date(2017, 4, 4, 11, 0, 0, 0) + const highTea = new Date(2017, 4, 4, 12, 0, 0, 3) + const firstProjectSuccessAction = { + type: PROJECT_PAGE_SUCCESS, + meta: { timestamp: brunch }, + payload: stalePayload + } + const secondProjectSuccessAction = { + type: PROJECT_PAGE_SUCCESS, + meta: { timestamp: highTea }, + payload: freshPayload + } + // The project result with the most recent timestamp should be maintained + const newestResults = versionReducer(undefined, secondProjectSuccessAction) + const withStaleAction = versionReducer(newestResults, + firstProjectSuccessAction) + + expect(withStaleAction.projectResultsTimestamp).toEqual(highTea) + expect(withStaleAction.TMMerge.projectVersions[0]) + .toEqual(newestResults.TMMerge.projectVersions[0]) + }) + it('can request a TM merge', () => { + const requestAction = { + type: VERSION_TM_MERGE_REQUEST + } + const mergeSuccessAction = { + type: VERSION_TM_MERGE_SUCCESS, + payload: { + messages: [], + percentageComplete: 100, + statusCode: 'Running', + url: 'http://localhost:8080/rest/process/key/TMMergeForVerKey-1-ja' + } + } + const initial = versionReducer(undefined, {type: 'any'}) + const mergeRequested = versionReducer(initial, requestAction) + const mergeReceived = versionReducer(mergeRequested, mergeSuccessAction) + expect(mergeReceived).toEqual({ + TMMerge: { + processStatus: { + messages: [], + percentageComplete: 100, + statusCode: 'Running', + url: 'http://localhost:8080/rest/process/key/TMMergeForVerKey-1-ja' + }, + projectVersions: [], + show: false, + triggered: false + }, + fetchingLocale: false, + fetchingProject: false, + locales: [], + notification: undefined, + projectResultsTimestamp: initial.projectResultsTimestamp + }) + }) + it('can track TM merge progress', () => { + const initial = versionReducer(undefined, { type: 'any' }) + const requestAction = versionReducer(initial, { + type: VERSION_TM_MERGE_REQUEST + }) + expect(requestAction.TMMerge.triggered).toEqual(true) + const failed = versionReducer(requestAction, { + type: VERSION_TM_MERGE_FAILURE + }) + expect(failed.TMMerge.processStatus).toEqual(undefined) + }) + + it('can cancel TM merge requests', () => { + const requestAction = { + type: TM_MERGE_CANCEL_REQUEST + } + const cancelSuccessAction = { + type: TM_MERGE_CANCEL_SUCCESS + } + const initial = versionReducer(undefined, {type: 'any'}) + const cancelRequested = versionReducer(initial, requestAction) + const cancelReceived = versionReducer(cancelRequested, cancelSuccessAction) + expect(cancelReceived).toEqual({ + TMMerge: { + processStatus: undefined, + projectVersions: [], + show: false, + triggered: false + }, + fetchingLocale: false, + fetchingProject: false, + locales: [], + notification: undefined, + projectResultsTimestamp: initial.projectResultsTimestamp + }) + }) + it('can Query TM merge progress', () => { + const initial = versionReducer(undefined, {type: 'any'}) + const queryRequestAction = { + type: QUERY_TM_MERGE_PROGRESS_REQUEST + } + const queryFailureAction = { + type: QUERY_TM_MERGE_PROGRESS_FAILURE + } + const queryRequested = versionReducer(initial, queryRequestAction) + const failureRecieved = versionReducer(queryRequested, queryFailureAction) + expect(failureRecieved).toEqual({ + TMMerge: { + processStatus: undefined, + projectVersions: [], + show: false, + triggered: false + }, + fetchingLocale: false, + fetchingProject: false, + locales: [], + notification: undefined, + projectResultsTimestamp: initial.projectResultsTimestamp + }) + }) + it('can handle TM Merge completion', () => { + const initial = versionReducer(undefined, {type: 'any'}) + const queryRequestAction = { + type: TM_MERGE_PROCESS_FINISHED + } + const statusRequested = versionReducer(initial, queryRequestAction) + expect(statusRequested.TMMerge.processStatus).toEqual(undefined) + }) +}) diff --git a/server/zanata-frontend/src/frontend/app/styles/style.less b/server/zanata-frontend/src/frontend/app/styles/style.less index dbd714c617..2b5e8fe9df 100644 --- a/server/zanata-frontend/src/frontend/app/styles/style.less +++ b/server/zanata-frontend/src/frontend/app/styles/style.less @@ -1028,8 +1028,8 @@ pre { display: block; margin: 0 0 @spacing-heading; padding: @spacing-large-horizontal; - word-wrap: break-word; - word-break: break-all; + word-break: normal; + word-break: break-word; /*Chrome specific*/ color: @gray-dark; border: 1px solid #ccc; border-radius: @border-radius-base; @@ -1351,6 +1351,10 @@ table td { font-size: 1.44rem; } +#messages { + margin-left: 4rem; +} + .locale-options { display: flex; display: -webkit-flex; @@ -1667,6 +1671,12 @@ td span.txt--understated { color: @color-danger; } +.modal-danger { + text-align: center; + padding-top: 1rem; + margin-bottom: 0; +} + .text-success { color: @color-success; } @@ -1698,12 +1708,14 @@ button.btn-link-sort i.fa.fa-sort { .vmerge-target { margin-left: 1em; } + .vmerge-target ul { list-style-type: none; margin-top: 0.5em; font-size: 1.1em; margin-bottom: 0; line-height:2em; + margin-left: 1rem; } .vmerge-boxes .panel-group { @@ -1721,6 +1733,7 @@ button.btn-link-sort i.fa.fa-sort { .vmerge-title { font-weight: 600; font-size: 1em; + margin-bottom: 0.375rem; } .vmerge-title .text-muted { @@ -1762,10 +1775,10 @@ label .s0.icon-locked { } .tm-panel .checkbox .label { - font-size: 0.95em; - margin-left: 1em; - padding-left: 0.375em; - padding-right: 0.375em; + font-size: 0.875rem; + margin-left: 1rem; + padding-left: 0.375rem; + padding-right: 0.375rem; } .tm-panel .checkbox small { @@ -2739,7 +2752,7 @@ button.btn-link.delete-link, button.btn-link.delete-link .loader-text { color: @color-danger; } -.btn-link, .btn-link.active, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link { +.btn-link, .btn-link, .btn-link.active, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link { background-color: transparent; -webkit-box-shadow: none; box-shadow: none; @@ -2893,19 +2906,19 @@ button.btn.btn-danger.dropdown-toggle > span.caret, button.btn.btn-info.dropdown } .label-success { - background-color: @label-success-bg; + background-color: @color-success; } .label-info { - background-color: @label-info-bg; + background-color: @color-light; } .label-warning { - background-color: @label-warning-bg; + background-color: @color-warning !important; } .label-danger { - background-color: @label-danger-bg; + background-color: @color-danger !important; } .dropdown { @@ -3352,6 +3365,11 @@ tbody.collapse { font-size: 1.2rem; } +.modal-content { + max-height: 90vh; + overflow-y: scroll; +} + .modal-select .Select-menu-outer { position: relative !important } @@ -4423,6 +4441,12 @@ img.avatar-round { } } +// Override legacy JSF new-zanata classes for TMMergeModal React component +@import "../containers/ProjectVersion/index.css"; + +// Override legacy JSF new-zanata classes for TMXModal React component +@import "../containers/TMX/index.css"; + // Nav variations // -------------------------------------------------- // Justified nav links @@ -5853,10 +5877,6 @@ a.lineclamp:after { margin: 0; } -.react-draggable.list-group-item span.text-muted { - margin-left: 1em; -} - .modal-body textarea.text-input { max-width: 100%; } @@ -6273,6 +6293,15 @@ button.close:focus, button.close:hover, button.close:active:focus { align-self: center; } +.new-zanata button.close { + -webkit-box-shadow: none; + box-shadow: none; + background-color: transparent; + border: none; + outline: none; + padding: 0.75rem +} + li.more-list-item { border-bottom: solid 1px #edf2f8; padding-top: @spacing-rq; @@ -6402,6 +6431,10 @@ i.i--left.i--lock, i.i--left.i--trash { padding-bottom: @spacing-base-and-a-half; } +.tm-panel ul.list-group { + padding:0; + margin:0; +} //Project Version Sidebar .accordion-section-title { @@ -6611,6 +6644,25 @@ table.tmx-table .badge .n1 { } +.translationContainer pre.cm-s-default { + word-wrap: normal; + word-break: normal; + word-break: break-word; /*Chrome specific*/ + -webkit-hyphens: initial; + -moz-hyphens: initial; + hyphens: initial; +} + +.new-zanata .button--group input[type=radio]:checked+.button--Off, +.new-zanata .button--group input[type=radio]:checked+.button--Off:hover { + background-color: @color-dark !important; + color: @gray-lighter; +} + +.panel__header a#new-version-link i.i.i--add:not(.i--large) { + color: @color-light; +} + // @screen-xs variable depreciated @media (max-width: 29.375rem) { .view { diff --git a/server/zanata-frontend/src/frontend/app/utils/EnumValueUtils.js b/server/zanata-frontend/src/frontend/app/utils/EnumValueUtils.js new file mode 100644 index 0000000000..f5869af1c6 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/utils/EnumValueUtils.js @@ -0,0 +1,20 @@ + +export const processStatusCodes = [ + 'NotAccepted', 'Waiting', 'Running', 'Finished', 'Cancelled', 'Failed'] + +const isStatusCodeEnded = statusCode => { + return statusCode === 'Finished' || statusCode === 'Cancelled' || + statusCode === 'Failed' +} +/** + * @param {{statusCode: string}} processStatus + * @returns {boolean} whether a process status represents an ended process + */ +export function isProcessEnded (processStatus) { + return processStatus && isStatusCodeEnded(processStatus.statusCode) +} + +export const entityStatuses = ['READONLY', 'ACTIVE', 'OBSOLETE'] +export function isEntityStatusReadOnly (status) { + return status === 'READONLY' +} diff --git a/server/zanata-frontend/src/frontend/app/utils/UrlHelper.js b/server/zanata-frontend/src/frontend/app/utils/UrlHelper.js index 2b473b0c5e..902fe5956a 100644 --- a/server/zanata-frontend/src/frontend/app/utils/UrlHelper.js +++ b/server/zanata-frontend/src/frontend/app/utils/UrlHelper.js @@ -25,6 +25,16 @@ export function getLanguageUrl (localeId) { return serverUrl + '/language/view/' + localeId } +/** + * @returns string of project version languages settings url + * e.g. https://translate.zanata.org/iteration/view/meikai/ + * ver1/settings/languages?dswid=4384 + */ +export function getVersionLanguageSettingsUrl (projectID, versionID) { + return serverUrl + '/iteration/view/' + projectID + '/' + versionID + + '/settings/languages' + getDswid() +} + export default { getDswid, getProjectUrl diff --git a/server/zanata-frontend/src/frontend/app/utils/prop-types-util.js b/server/zanata-frontend/src/frontend/app/utils/prop-types-util.js new file mode 100644 index 0000000000..9371641281 --- /dev/null +++ b/server/zanata-frontend/src/frontend/app/utils/prop-types-util.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import {processStatusCodes, entityStatuses} from './EnumValueUtils' + +export const entityStatusPropType = PropTypes.oneOf(entityStatuses) + +export const versionDtoPropType = PropTypes.shape({ + id: PropTypes.string.isRequired, + status: entityStatusPropType +}) + +export const ProjectType = PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + versions: PropTypes.arrayOf(versionDtoPropType).isRequired +}) + +export const LocaleType = PropTypes.shape({ + displayName: PropTypes.string.isRequired, + localeId: PropTypes.string.isRequired, + nativeName: PropTypes.string.isRequired +}) + +export const FromProjectVersionType = PropTypes.shape({ + projectSlug: PropTypes.string.isRequired, + version: versionDtoPropType.isRequired +}) + +export const processStatusCodeType = PropTypes.oneOf(processStatusCodes) + +export const processStatusType = PropTypes.shape({ + url: PropTypes.string.isRequired, + percentageComplete: PropTypes.number.isRequired, + statusCode: processStatusCodeType.isRequired +}) diff --git a/server/zanata-frontend/src/frontend/jest.config.json b/server/zanata-frontend/src/frontend/jest.config.json new file mode 100644 index 0000000000..cce5ed2ee0 --- /dev/null +++ b/server/zanata-frontend/src/frontend/jest.config.json @@ -0,0 +1,31 @@ +{ + "collectCoverageFrom": [ + "app/**/*.{js,jsx}", + "!**/node_modules/**", + "!app/**/*.story.js" + ], + "coverageReporters": [ + "cobertura", + "html", + "lcov", + "text" + ], + "moduleNameMapper": { + "^.+\\.(css)$": "./__tests__/mock/mockCss.js" + }, + "transform": { + ".*": "./node_modules/babel-jest" + }, + "testPathIgnorePatterns": [ + "/node_modules/", + "/__tests__/mock" + ], + "unmockedModulePathPatterns": [ + "/node_modules", + "/app" + ], + "moduleFileExtensions": [ + "js", + "jsx" + ] +} diff --git a/server/zanata-frontend/src/frontend/makefile b/server/zanata-frontend/src/frontend/makefile index 225a10ad79..13c8528387 100644 --- a/server/zanata-frontend/src/frontend/makefile +++ b/server/zanata-frontend/src/frontend/makefile @@ -58,9 +58,17 @@ test: yarn test # Run the tests on every change +# Specify coverage=path if you want to limit reported coverage scope. This is +# useful when using pattern mode to limit the tests that are run. +# e.g. make test-watch coverage="app/editor/reducers/*" +# p > app/editor/reducers/.* +# NOTE coverage uses a glob pattern (*), but test pattern uses a regex (.*) test-watch: +ifdef coverage + yarn test:watch -- --collectCoverageFrom=$(coverage) +else yarn test:watch - +endif # Run a server that implements some of the Zanata API with some dummy data. # The server runs on localhost:7878 and has 0.5 to 5s of random latency. diff --git a/server/zanata-frontend/src/frontend/package.json b/server/zanata-frontend/src/frontend/package.json index fca923fc6b..a9221cd0c2 100644 --- a/server/zanata-frontend/src/frontend/package.json +++ b/server/zanata-frontend/src/frontend/package.json @@ -14,7 +14,8 @@ "storybook-editor": "start-storybook -p 9001 -s ./dist --config-dir .storybook-editor", "storybook-frontend": "start-storybook -p 9001 -s ./dist --config-dir .storybook-frontend", "storybook-build": "build-storybook -s ./dist", - "test": "jest --coverage" + "test": "jest --coverage --config jest.config.json", + "test:watch": "jest --watch --coverage --config jest.config.json" }, "repository": { "type": "git", @@ -114,6 +115,7 @@ "react-router": "3.0.5", "react-router-redux": "4.0.8", "react-select": "1.0.0-rc.5", + "react-sortable-hoc": "0.6.3", "react-textarea-autosize": "4.3.2", "redux": "3.5.2", "redux-actions": "0.10.0", @@ -131,36 +133,5 @@ "svg-sprite": "1.3.6", "text-diff": "1.0.1", "webfontloader": "1.6.24" - }, - "jest": { - "collectCoverageFrom": [ - "app/**/*.{js,jsx}", - "!**/node_modules/**", - "!app/**/*.story.js" - ], - "coverageReporters": [ - "cobertura", - "html", - "lcov", - "text" - ], - "moduleNameMapper": { - "^.+\\.(css)$": "./__tests__/mock/mockCss.js" - }, - "transform": { - ".*": "./node_modules/babel-jest" - }, - "testPathIgnorePatterns": [ - "/node_modules/", - "/__tests__/mock" - ], - "unmockedModulePathPatterns": [ - "/node_modules", - "/app" - ], - "moduleFileExtensions": [ - "js", - "jsx" - ] } } diff --git a/server/zanata-frontend/src/frontend/scripts/codemods/stateful.template.js b/server/zanata-frontend/src/frontend/scripts/codemods/stateful.template.js index 9eb6a98e70..245f7651af 100644 --- a/server/zanata-frontend/src/frontend/scripts/codemods/stateful.template.js +++ b/server/zanata-frontend/src/frontend/scripts/codemods/stateful.template.js @@ -25,8 +25,9 @@ class COMPONENT_NAME_HERE extends Component { } onClick = () => { - const clicks = this.state.clicks + 1 - this.setState({ clicks }) + this.setState(prevState => ({ + clicks: prevState.clicks + 1 + })) this.props.onClick(`The cow says ${this.props.noise}`) } diff --git a/server/zanata-frontend/src/frontend/yarn.lock b/server/zanata-frontend/src/frontend/yarn.lock index d638371c41..8499473092 100644 --- a/server/zanata-frontend/src/frontend/yarn.lock +++ b/server/zanata-frontend/src/frontend/yarn.lock @@ -4960,7 +4960,7 @@ lodash@4.13.1, lodash@^4.11.2, lodash@^4.2.0, lodash@^4.2.1: version "4.13.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.13.1.tgz#83e4b10913f48496d4d16fec4a560af2ee744b68" -lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.3.0, lodash@^4.6.1: +lodash@4.x.x, lodash@^4.0.0, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.3.0, lodash@^4.6.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -6461,6 +6461,15 @@ react-simple-di@^1.2.0: babel-runtime "6.x.x" hoist-non-react-statics "1.x.x" +react-sortable-hoc@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.6.3.tgz#ab9fa699607c5f5871d00889222fb52559242947" + dependencies: + babel-runtime "^6.11.6" + invariant "^2.2.1" + lodash "^4.12.0" + prop-types "^15.5.7" + react-split-pane@0.1.63: version "0.1.63" resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.63.tgz#fadb3960cc659911dd05ffbc88acee4be9f53583" diff --git a/server/zanata-model/pom.xml b/server/zanata-model/pom.xml index f3416fb96a..2ab790f93f 100644 --- a/server/zanata-model/pom.xml +++ b/server/zanata-model/pom.xml @@ -101,15 +101,6 @@ jaxrs-api
    - - - org.hamcrest - hamcrest-core - - - org.hamcrest - hamcrest-library - javax.xml.stream stax-api diff --git a/server/zanata-model/src/main/java/org/zanata/hibernate/search/IndexFieldLabels.java b/server/zanata-model/src/main/java/org/zanata/hibernate/search/IndexFieldLabels.java index ae8dd9832b..b7d56cdf6a 100644 --- a/server/zanata-model/src/main/java/org/zanata/hibernate/search/IndexFieldLabels.java +++ b/server/zanata-model/src/main/java/org/zanata/hibernate/search/IndexFieldLabels.java @@ -36,4 +36,8 @@ public interface IndexFieldLabels { public static final String GLOSSARY_QUALIFIED_NAME = "glossaryEntry.glossary.qualifiedName"; String PROJECT_VERSION_ID_FIELD = "projectVersionId"; + /* + * Represents the full slug without tokenize + */ + String FULL_SLUG_FIELD = "fullSlug"; } diff --git a/server/zanata-model/src/main/java/org/zanata/model/SlugEntityBase.java b/server/zanata-model/src/main/java/org/zanata/model/SlugEntityBase.java index 6a6459aadd..5fc2e3f783 100644 --- a/server/zanata-model/src/main/java/org/zanata/model/SlugEntityBase.java +++ b/server/zanata-model/src/main/java/org/zanata/model/SlugEntityBase.java @@ -27,8 +27,15 @@ import javax.persistence.Transient; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; + +import org.apache.lucene.analysis.core.SimpleAnalyzer; import org.hibernate.annotations.NaturalId; +import org.hibernate.search.annotations.Analyze; +import org.hibernate.search.annotations.Analyzer; import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.Fields; +import org.zanata.hibernate.search.CaseInsensitiveWhitespaceAnalyzer; +import org.zanata.hibernate.search.IndexFieldLabels; import org.zanata.model.validator.Slug; import com.google.common.annotations.VisibleForTesting; @@ -48,7 +55,10 @@ public abstract class SlugEntityBase extends ModelEntityBase { @Size(min = 1, max = 40) @Slug @NotNull - @Field + @Fields({ + @Field, + @Field(analyzer = @Analyzer(impl = CaseInsensitiveWhitespaceAnalyzer.class), name = IndexFieldLabels.FULL_SLUG_FIELD) + }) private String slug; /** diff --git a/server/zanata-model/src/main/java/org/zanata/model/tm/TransMemory.java b/server/zanata-model/src/main/java/org/zanata/model/tm/TransMemory.java index 6226977ca0..461ac467c6 100644 --- a/server/zanata-model/src/main/java/org/zanata/model/tm/TransMemory.java +++ b/server/zanata-model/src/main/java/org/zanata/model/tm/TransMemory.java @@ -34,6 +34,8 @@ import javax.persistence.MapKeyColumn; import javax.persistence.MapKeyEnumerated; import javax.persistence.OneToMany; +import javax.validation.constraints.Size; + import org.zanata.model.SlugEntityBase; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -48,7 +50,7 @@ @Access(AccessType.FIELD) public class TransMemory extends SlugEntityBase implements HasTMMetadata { private static final long serialVersionUID = 1L; - @Column(columnDefinition = "longtext") + @Size(max = 100) private String description; // This is the BCP-47 language code. Null means any source language (*all* // in TMX) diff --git a/server/zanata-model/src/test/java/org/zanata/model/HTextFlowTest.java b/server/zanata-model/src/test/java/org/zanata/model/HTextFlowTest.java index d1debcc95a..b9b7d554a8 100644 --- a/server/zanata-model/src/test/java/org/zanata/model/HTextFlowTest.java +++ b/server/zanata-model/src/test/java/org/zanata/model/HTextFlowTest.java @@ -21,12 +21,10 @@ package org.zanata.model; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; - -import org.hamcrest.Matchers; import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Patrick Huang pahuang@redhat.com @@ -37,20 +35,20 @@ public void testSetContents() throws Exception { HTextFlow textFlow = new HTextFlow(); textFlow.setContents("a", "b"); - assertThat(textFlow.getContents(), contains("a", "b")); + assertThat(textFlow.getContents()).contains("a", "b"); textFlow.setContents("a"); - assertThat(textFlow.getContents(), contains("a")); + assertThat(textFlow.getContents()).contains("a"); // check that content1 is nulled out (after having been non-null // earlier) - assertThat(textFlow.getContent1(), Matchers.nullValue()); + assertThat(textFlow.getContent1()).isNull(); // set original value textFlow.setContents("a", "b"); - assertThat(textFlow.getContents(), contains("a", "b")); + assertThat(textFlow.getContents()).contains("a", "b"); // set same value textFlow.setContents("a", "b"); - assertThat(textFlow.getContents(), contains("a", "b")); + assertThat(textFlow.getContents()).contains("a", "b"); } } diff --git a/server/zanata-model/src/test/java/org/zanata/model/SlugEntityBaseTest.java b/server/zanata-model/src/test/java/org/zanata/model/SlugEntityBaseTest.java index da971b7a11..46a2a945ce 100644 --- a/server/zanata-model/src/test/java/org/zanata/model/SlugEntityBaseTest.java +++ b/server/zanata-model/src/test/java/org/zanata/model/SlugEntityBaseTest.java @@ -20,14 +20,11 @@ */ package org.zanata.model; -import org.assertj.core.api.Assertions; import org.junit.Test; import java.util.Date; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Patrick Huang @@ -92,23 +89,23 @@ public void toStringAndEqualsTest() { entity.setVersionNum(2); entity.setCreationDate(now); entity.setLastChanged(now); - assertThat(entity.toString(), - containsString("[id=1,versionNum=2], slug=abc)")); + assertThat(entity.toString()). + contains("[id=1,versionNum=2], slug=abc)"); SlugEntityBase other = new SlugClass("abc"); other.setCreationDate(now); other.setLastChanged(now); - assertThat(entity.equals(other), equalTo(false)); + assertThat(entity.equals(other)).isFalse(); other.setId(entity.getId()); other.setVersionNum(entity.getVersionNum()); - assertThat(entity, equalTo(other)); - assertThat(entity.hashCode(), equalTo(other.hashCode())); + assertThat(entity).isEqualTo(other); + assertThat(entity.hashCode()).isEqualTo(other.hashCode()); } @Test public void changeToDeletedSlug() { SlugEntityBase slugEntityBase = new SlugClass("abc"); String newSlug = slugEntityBase.changeToDeletedSlug(); - Assertions.assertThat(newSlug).isEqualTo("abc" + DELETED_SUFFIX); + assertThat(newSlug).isEqualTo("abc" + DELETED_SUFFIX); } @Test @@ -117,7 +114,7 @@ public void canChangeToDeletedSlugWithSuffixInPlaceIfOldSlugIsTooLong() { SlugEntityBase slugEntityBase = new SlugClass("abcdefghijklmnopqrstuvwxyz1234567890"); String newSlug = slugEntityBase.changeToDeletedSlug(); - Assertions.assertThat(newSlug) + assertThat(newSlug) .isEqualTo("abcdefghijklmnopqrstuvwxyz1" + DELETED_SUFFIX) .hasSize(40); } diff --git a/server/zanata-model/src/test/java/org/zanata/model/StatusCountTest.java b/server/zanata-model/src/test/java/org/zanata/model/StatusCountTest.java index 3bd8fe9788..1a3c4c19dc 100644 --- a/server/zanata-model/src/test/java/org/zanata/model/StatusCountTest.java +++ b/server/zanata-model/src/test/java/org/zanata/model/StatusCountTest.java @@ -23,8 +23,7 @@ import org.junit.Test; import org.zanata.common.ContentState; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Patrick Huang {\\b Special} text"), - equalTo("Special text")); + removeFormattingMarkup( + "{\\b Special} text")) + .isEqualTo("Special text"); assertThat( - removeFormattingMarkup("{\\cf7 Special} text"), - equalTo("Special text")); + removeFormattingMarkup("{\\cf7 Special} text")). + isEqualTo("Special text"); assertThat( - removeFormattingMarkup("<B>Special</B> text"), - equalTo("Special text")); + removeFormattingMarkup("<B>Special</B> text")). + isEqualTo("Special text"); assertThat( removeFormattingMarkup("The <i><b>" - + "big</b> black</i> cat."), - equalTo("The big black cat.")); + + "big</b> black</i> cat.")). + isEqualTo("The big black cat."); assertThat( removeFormattingMarkup("The icon <img src=\"testNode.gif\"/>represents " - + "a conditional node."), - equalTo("The icon represents a conditional node.")); + + "a conditional node.")). + isEqualTo("The icon represents a conditional node."); } @Test @@ -39,8 +39,7 @@ public void extractPlainTextContentWithNestedFootnote() throws Exception { + "\\s15\\widctlpar \\f4\\fs20" + "{\\cs16\\super \\chftn } An elephant is a very " + "large animal.}} are big. "; - assertThat( - removeFormattingMarkup(segXML), - equalTo("Elephants are big.")); + assertThat(removeFormattingMarkup(segXML)) + .isEqualTo("Elephants are big."); } } diff --git a/server/zanata-war/pom.xml b/server/zanata-war/pom.xml index bfecfafae2..3fd07b8369 100644 --- a/server/zanata-war/pom.xml +++ b/server/zanata-war/pom.xml @@ -856,11 +856,6 @@ - - org.zanata - zanata-assets - - org.zanata zanata-frontend @@ -1407,18 +1402,6 @@ provided - - org.hamcrest - hamcrest-core - test - - - - org.hamcrest - hamcrest-library - test - - javax.activation activation diff --git a/server/zanata-war/src/main/java/org/zanata/WebAssetsConfiguration.java b/server/zanata-war/src/main/java/org/zanata/WebAssetsConfiguration.java deleted file mode 100644 index 9932ea6541..0000000000 --- a/server/zanata-war/src/main/java/org/zanata/WebAssetsConfiguration.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2015, Red Hat, Inc. and individual contributors as indicated by the - * @author tags. See the copyright.txt file in the distribution for a full - * listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - * - * This software 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 Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this software; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF - * site: http://www.fsf.org. - */ - -package org.zanata; - -import org.apache.commons.lang.StringUtils; -import javax.annotation.PostConstruct; -import javax.inject.Named; - -import javax.faces.application.ResourceHandler; -import javax.faces.context.FacesContext; -import java.util.AbstractMap; -import java.util.Collections; -import java.util.Set; - -/** - * Utility component for accessing zanata-assets resources from JSF/HTML page. - * - * Usage in JSF/HTML page: - * Rendered URL from example above is: {@link #DEFAULT_WEB_ASSETS_URL}/img/logo/logo.ico - * - * {@link #DEFAULT_WEB_ASSETS_URL} can be overridden in system property {@link #ASSETS_PROPERTY_KEY} - * - * - * @author Alex Eng aeng@redhat.com - */ - -@Named("assets") -@javax.enterprise.context.ApplicationScoped -public class WebAssetsConfiguration extends AbstractMap { - - /** - * system property for zanata assets url - */ - private final static String ASSETS_PROPERTY_KEY = "zanata.assets.url"; - - /** - * Default url for zanata-assets, http://{zanata.url}/javax.faces.resource/jars/assets - */ - private final static String DEFAULT_WEB_ASSETS_URL = String.format("%s%s/%s/%s", - FacesContext.getCurrentInstance().getExternalContext() - .getRequestContextPath(), - ResourceHandler.RESOURCE_IDENTIFIER, "jars", "assets"); - - private String webAssetsUrlBase; - - @PostConstruct - public void postConstruct() { - String assetsProperty = System.getProperty(ASSETS_PROPERTY_KEY); - - /** - * Try with system property of {@link #ASSETS_PROPERTY_KEY} if exists, - * otherwise {@link #DEFAULT_WEB_ASSETS_URL} - */ - webAssetsUrlBase = - StringUtils.isEmpty(assetsProperty) ? DEFAULT_WEB_ASSETS_URL - : assetsProperty; - } - - private String getWebAssetsUrl(String resource) { - return webAssetsUrlBase + "/" + resource; - } - - @Override - public String get(Object key) { - return getWebAssetsUrl((String) key); - } - - @Override - public Set> entrySet() { - return Collections.emptySet(); - } -} diff --git a/server/zanata-war/src/main/java/org/zanata/action/CopyTransManager.java b/server/zanata-war/src/main/java/org/zanata/action/CopyTransManager.java index ab940174d7..1fab1f35d2 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/CopyTransManager.java +++ b/server/zanata-war/src/main/java/org/zanata/action/CopyTransManager.java @@ -20,20 +20,26 @@ */ package org.zanata.action; +import static org.zanata.async.AsyncTaskKey.joinFields; + import java.io.Serializable; + import javax.annotation.Nonnull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.enterprise.context.Dependent; import javax.inject.Inject; -import javax.inject.Named; + import org.zanata.async.AsyncTaskHandle; import org.zanata.async.AsyncTaskHandleManager; +import org.zanata.async.AsyncTaskKey; +import org.zanata.async.GenericAsyncTaskKey; import org.zanata.async.handle.CopyTransTaskHandle; import org.zanata.model.HCopyTransOptions; import org.zanata.model.HDocument; import org.zanata.model.HProjectIteration; import org.zanata.security.ZanataIdentity; import org.zanata.service.CopyTransService; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; // TODO This class should be merged with the copy trans service (?) /** @@ -59,7 +65,7 @@ public class CopyTransManager implements Serializable { private ZanataIdentity identity; public boolean isCopyTransRunning(@Nonnull Object target) { - CopyTransProcessKey key; + AsyncTaskKey key; if (target instanceof HProjectIteration) { key = CopyTransProcessKey.getKey((HProjectIteration) target); } else if (target instanceof HDocument) { @@ -93,8 +99,9 @@ public void startCopyTrans(HDocument document, HCopyTransOptions options) { "Copy Trans is already running for document \'" + document.getDocId() + "\'"); } - CopyTransProcessKey key = CopyTransProcessKey.getKey(document); + AsyncTaskKey key = CopyTransProcessKey.getKey(document); CopyTransTaskHandle handle = new CopyTransTaskHandle(); + handle.setTriggeredBy(identity.getAccountUsername()); asyncTaskHandleManager.registerTaskHandle(handle, key); copyTransServiceImpl.startCopyTransForDocument(document, options, handle); @@ -111,8 +118,9 @@ public void startCopyTrans(HProjectIteration iteration, "Copy Trans is already running for version \'" + iteration.getSlug() + "\'"); } - CopyTransProcessKey key = CopyTransProcessKey.getKey(iteration); + AsyncTaskKey key = CopyTransProcessKey.getKey(iteration); CopyTransTaskHandle handle = new CopyTransTaskHandle(); + handle.setTriggeredBy(identity.getAccountUsername()); asyncTaskHandleManager.registerTaskHandle(handle, key); copyTransServiceImpl.startCopyTransForIteration(iteration, options, handle); @@ -120,7 +128,7 @@ public void startCopyTrans(HProjectIteration iteration, public CopyTransTaskHandle getCopyTransProcessHandle(@Nonnull Object target) { - CopyTransProcessKey key; + AsyncTaskKey key; if (target instanceof HProjectIteration) { key = CopyTransProcessKey.getKey((HProjectIteration) target); } else if (target instanceof HDocument) { @@ -145,93 +153,21 @@ public void cancelCopyTrans(@Nonnull HProjectIteration iteration) { /** * Internal class to index Copy Trans processes. */ - private static final class CopyTransProcessKey implements Serializable { - private static final long serialVersionUID = -2054359069473618887L; - private String projectSlug; - private String iterationSlug; - private String docId; - - public static CopyTransProcessKey getKey(HProjectIteration iteration) { - CopyTransProcessKey newKey = new CopyTransProcessKey(); - newKey.setProjectSlug(iteration.getProject().getSlug()); - newKey.setIterationSlug(iteration.getSlug()); - return newKey; - } + private static final class CopyTransProcessKey { + private static final String KEY_NAME = "copyTransKey"; - public static CopyTransProcessKey getKey(HDocument document) { - CopyTransProcessKey newKey = new CopyTransProcessKey(); - newKey.setDocId(document.getDocId()); - newKey.setProjectSlug( - document.getProjectIteration().getProject().getSlug()); - newKey.setIterationSlug(document.getProjectIteration().getSlug()); - return newKey; - } - @Override - public boolean equals(final Object o) { - if (o == this) - return true; - if (!(o instanceof CopyTransManager.CopyTransProcessKey)) - return false; - final CopyTransProcessKey other = (CopyTransProcessKey) o; - final Object this$projectSlug = this.getProjectSlug(); - final Object other$projectSlug = other.getProjectSlug(); - if (this$projectSlug == null ? other$projectSlug != null - : !this$projectSlug.equals(other$projectSlug)) - return false; - final Object this$iterationSlug = this.getIterationSlug(); - final Object other$iterationSlug = other.getIterationSlug(); - if (this$iterationSlug == null ? other$iterationSlug != null - : !this$iterationSlug.equals(other$iterationSlug)) - return false; - final Object this$docId = this.getDocId(); - final Object other$docId = other.getDocId(); - if (this$docId == null ? other$docId != null - : !this$docId.equals(other$docId)) - return false; - return true; + public static AsyncTaskKey getKey(HProjectIteration iteration) { + return new GenericAsyncTaskKey(joinFields(iteration.getProject().getSlug(), + iteration.getSlug(), null)); } - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - final Object $projectSlug = this.getProjectSlug(); - result = result * PRIME - + ($projectSlug == null ? 43 : $projectSlug.hashCode()); - final Object $iterationSlug = this.getIterationSlug(); - result = result * PRIME - + ($iterationSlug == null ? 43 : $iterationSlug.hashCode()); - final Object $docId = this.getDocId(); - result = result * PRIME + ($docId == null ? 43 : $docId.hashCode()); - return result; + public static AsyncTaskKey getKey(HDocument document) { + String projectSlug = document.getProjectIteration().getProject().getSlug(); + String versionSlug = document.getProjectIteration().getSlug(); + String docId = document.getDocId(); + return new GenericAsyncTaskKey(joinFields(KEY_NAME, projectSlug, versionSlug, docId)); } - public String getProjectSlug() { - return this.projectSlug; - } - - public String getIterationSlug() { - return this.iterationSlug; - } - - public String getDocId() { - return this.docId; - } - - public void setProjectSlug(final String projectSlug) { - this.projectSlug = projectSlug; - } - - public void setIterationSlug(final String iterationSlug) { - this.iterationSlug = iterationSlug; - } - - public void setDocId(final String docId) { - this.docId = docId; - } - - private CopyTransProcessKey() { - } } } diff --git a/server/zanata-war/src/main/java/org/zanata/action/CopyVersionManager.java b/server/zanata-war/src/main/java/org/zanata/action/CopyVersionManager.java index 7fb1170d87..adc0386eb2 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/CopyVersionManager.java +++ b/server/zanata-war/src/main/java/org/zanata/action/CopyVersionManager.java @@ -1,14 +1,21 @@ package org.zanata.action; +import static org.zanata.async.AsyncTaskKey.joinFields; + import java.io.Serializable; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import javax.enterprise.context.Dependent; import javax.inject.Inject; + import org.zanata.async.AsyncTaskHandleManager; +import org.zanata.async.AsyncTaskKey; +import org.zanata.async.GenericAsyncTaskKey; import org.zanata.async.handle.CopyVersionTaskHandle; import org.zanata.security.ZanataIdentity; import org.zanata.service.CopyVersionService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * Manages copy version tasks. * @@ -41,7 +48,7 @@ public class CopyVersionManager implements Serializable { */ public void startCopyVersion(String projectSlug, String versionSlug, String newVersionSlug) { - CopyVersionKey key = CopyVersionKey.getKey(projectSlug, newVersionSlug); + AsyncTaskKey key = CopyVersionKey.getKey(projectSlug, newVersionSlug); CopyVersionTaskHandle handle = new CopyVersionTaskHandle(); asyncTaskHandleManager.registerTaskHandle(handle, key); copyVersionServiceImpl.startCopyVersion(projectSlug, versionSlug, @@ -83,68 +90,22 @@ public boolean isCopyVersionRunning(String projectSlug, /** * Key used for copy version task * - * @param projectSlug - * - target project identifier - * @param versionSlug - * - target version identifier */ - public static final class CopyVersionKey implements Serializable { - // target project identifier - private final String projectSlug; - // target version identifier - private final String versionSlug; + public static final class CopyVersionKey { + private static final String KEY_NAME = "copyVersion"; - public static CopyVersionKey getKey(String projectSlug, - String versionSlug) { - return new CopyVersionKey(projectSlug, versionSlug); + /** + * + * @param projectSlug + * - target project identifier + * @param versionSlug + * - target version identifier + */ + public static AsyncTaskKey + getKey(String projectSlug, String versionSlug) { + return new GenericAsyncTaskKey( + joinFields(KEY_NAME, projectSlug, versionSlug)); } - @Override - public boolean equals(final Object o) { - if (o == this) - return true; - if (!(o instanceof CopyVersionManager.CopyVersionKey)) - return false; - final CopyVersionKey other = (CopyVersionKey) o; - final Object this$projectSlug = this.getProjectSlug(); - final Object other$projectSlug = other.getProjectSlug(); - if (this$projectSlug == null ? other$projectSlug != null - : !this$projectSlug.equals(other$projectSlug)) - return false; - final Object this$versionSlug = this.getVersionSlug(); - final Object other$versionSlug = other.getVersionSlug(); - if (this$versionSlug == null ? other$versionSlug != null - : !this$versionSlug.equals(other$versionSlug)) - return false; - return true; - } - - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - final Object $projectSlug = this.getProjectSlug(); - result = result * PRIME - + ($projectSlug == null ? 43 : $projectSlug.hashCode()); - final Object $versionSlug = this.getVersionSlug(); - result = result * PRIME - + ($versionSlug == null ? 43 : $versionSlug.hashCode()); - return result; - } - - public String getProjectSlug() { - return this.projectSlug; - } - - public String getVersionSlug() { - return this.versionSlug; - } - - @java.beans.ConstructorProperties({ "projectSlug", "versionSlug" }) - public CopyVersionKey(final String projectSlug, - final String versionSlug) { - this.projectSlug = projectSlug; - this.versionSlug = versionSlug; - } } } diff --git a/server/zanata-war/src/main/java/org/zanata/action/LanguageAction.java b/server/zanata-war/src/main/java/org/zanata/action/LanguageAction.java index 0833edbe3c..7bc21da35d 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/LanguageAction.java +++ b/server/zanata-war/src/main/java/org/zanata/action/LanguageAction.java @@ -207,6 +207,15 @@ public void addSelected() { resetLocale(); } + public boolean isAnySelected() { + for (SelectablePerson selectablePerson : getSearchResults()) { + if (selectablePerson.isSelected()) { + return true; + } + } + return false; + } + public String getPluralsPlaceholder() { String pluralForms = resourceUtils .getPluralForms(new LocaleId(language), false, true); diff --git a/server/zanata-war/src/main/java/org/zanata/action/MergeTranslationsManager.java b/server/zanata-war/src/main/java/org/zanata/action/MergeTranslationsManager.java index 24d763ffd7..0fd57cecfb 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/MergeTranslationsManager.java +++ b/server/zanata-war/src/main/java/org/zanata/action/MergeTranslationsManager.java @@ -1,15 +1,21 @@ package org.zanata.action; import java.io.Serializable; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import javax.enterprise.context.Dependent; import javax.inject.Inject; -import javax.inject.Named; + import org.zanata.async.AsyncTaskHandleManager; +import org.zanata.async.AsyncTaskKey; +import org.zanata.async.GenericAsyncTaskKey; import org.zanata.async.handle.MergeTranslationsTaskHandle; import org.zanata.security.ZanataIdentity; import org.zanata.service.MergeTranslationsService; +import static org.zanata.async.AsyncTaskKey.joinFields; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * Manages tasks to copy translations from one existing version to another. * @@ -48,8 +54,10 @@ public class MergeTranslationsManager implements Serializable { public void start(String sourceProjectSlug, String sourceVersionSlug, String targetProjectSlug, String targetVersionSlug, boolean useNewerTranslation) { - Key key = Key.getKey(targetProjectSlug, targetVersionSlug); - MergeTranslationsTaskHandle handle = new MergeTranslationsTaskHandle(); + AsyncTaskKey + key = MergeVersionKey + .getKey(targetProjectSlug, targetVersionSlug); + MergeTranslationsTaskHandle handle = new MergeTranslationsTaskHandle(key); asyncTaskHandleManager.registerTaskHandle(handle, key); mergeTranslationsServiceImpl.startMergeTranslations(sourceProjectSlug, sourceVersionSlug, targetProjectSlug, targetVersionSlug, @@ -79,7 +87,7 @@ public void cancel(String projectSlug, String versionSlug) { public MergeTranslationsTaskHandle getProcessHandle(String projectSlug, String versionSlug) { return (MergeTranslationsTaskHandle) asyncTaskHandleManager - .getHandleByKey(Key.getKey(projectSlug, versionSlug)); + .getHandleByKey(MergeVersionKey.getKey(projectSlug, versionSlug)); } public boolean isRunning(String projectSlug, String versionSlug) { @@ -89,63 +97,16 @@ public boolean isRunning(String projectSlug, String versionSlug) { } /** - * Key used for copy version task + * Key used for merge version task */ - public static final class Key implements Serializable { - // target project identifier - private final String projectSlug; - // target version identifier - private final String versionSlug; - - public static Key getKey(String projectSlug, String versionSlug) { - return new Key(projectSlug, versionSlug); - } + public static final class MergeVersionKey { + private static final String KEY_NAME = "mergeVersion"; - @Override - public boolean equals(final Object o) { - if (o == this) - return true; - if (!(o instanceof MergeTranslationsManager.Key)) - return false; - final Key other = (Key) o; - final Object this$projectSlug = this.getProjectSlug(); - final Object other$projectSlug = other.getProjectSlug(); - if (this$projectSlug == null ? other$projectSlug != null - : !this$projectSlug.equals(other$projectSlug)) - return false; - final Object this$versionSlug = this.getVersionSlug(); - final Object other$versionSlug = other.getVersionSlug(); - if (this$versionSlug == null ? other$versionSlug != null - : !this$versionSlug.equals(other$versionSlug)) - return false; - return true; + public static AsyncTaskKey getKey(String projectSlug, String versionSlug) { + return new GenericAsyncTaskKey(joinFields(KEY_NAME, projectSlug, versionSlug)); } - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - final Object $projectSlug = this.getProjectSlug(); - result = result * PRIME - + ($projectSlug == null ? 43 : $projectSlug.hashCode()); - final Object $versionSlug = this.getVersionSlug(); - result = result * PRIME - + ($versionSlug == null ? 43 : $versionSlug.hashCode()); - return result; - } + } - public String getProjectSlug() { - return this.projectSlug; - } - public String getVersionSlug() { - return this.versionSlug; - } - - @java.beans.ConstructorProperties({ "projectSlug", "versionSlug" }) - public Key(final String projectSlug, final String versionSlug) { - this.projectSlug = projectSlug; - this.versionSlug = versionSlug; - } - } } diff --git a/server/zanata-war/src/main/java/org/zanata/action/ProjectHome.java b/server/zanata-war/src/main/java/org/zanata/action/ProjectHome.java index 86cded07c3..aa903485e9 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/ProjectHome.java +++ b/server/zanata-war/src/main/java/org/zanata/action/ProjectHome.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.ResourceBundle; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.enterprise.inject.Any; import javax.enterprise.inject.Model; @@ -205,16 +206,14 @@ public List getDisabledLocales() { * already in the project. */ private List findActiveNotEnabledLocales() { - Collection filtered = Collections2 - .filter(localeDAO.findAllActive(), new Predicate() { - - @Override - public boolean apply(HLocale input) { - // only include those not already in the project - return !getEnabledLocales().contains(input); - } - }); - return Lists.newArrayList(filtered); + List activeLocales = localeDAO.findAllActive(); + // only include those not already in the project + List filteredList = activeLocales.stream() + .filter(hLocale -> !getEnabledLocales().contains(hLocale)) + .collect( + Collectors.toList()); + Collections.sort(filteredList, ComparatorUtil.LOCALE_COMPARATOR); + return filteredList; } private Map roleRestrictions; @@ -688,6 +687,28 @@ public void initialize() { } } + public void onProjectNameChange(ValueChangeEvent e) { + if (!isValidName((String) e.getNewValue())) { + String componentId = e.getComponent().getId(); + facesMessages.addToControl(componentId, + msgs.get("jsf.project.name.validation.alphanumeric")); + } + } + + /** + * Check the name by removing any whitespaces in the string and + * make sure it contains at least an alphanumeric char + */ + public boolean isValidName(String name) { + String trimmedName = StringUtils.deleteWhitespace(name); + for (char c : trimmedName.toCharArray()) { + if (Character.isDigit(c) || Character.isLetter(c)) { + return true; + } + } + return false; + } + public void verifySlugAvailable(ValueChangeEvent e) { String slug = (String) e.getNewValue(); validateSlug(slug, e.getComponent().getId()); @@ -740,6 +761,11 @@ public String update() { && !getSlug().equals(getInputSlugValue())) { getInstance().setSlug(getInputSlugValue()); } + if (!isValidName(getInstance().getName())) { + facesMessages.addGlobal(SEVERITY_ERROR, + msgs.get("jsf.project.name.validation.alphanumeric")); + return null; + } boolean softDeleted = false; if (getInstance().getStatus() == EntityStatus.OBSOLETE) { softDeleted = true; @@ -768,6 +794,11 @@ public String persist() { return null; } getInstance().setSlug(getInputSlugValue()); + if (!isValidName(getInstance().getName())) { + facesMessages.addGlobal(SEVERITY_ERROR, + msgs.get("jsf.project.name.validation.alphanumeric")); + return null; + } if (StringUtils.isEmpty(selectedProjectType) || selectedProjectType.equals("null")) { facesMessages.addGlobal(SEVERITY_ERROR, diff --git a/server/zanata-war/src/main/java/org/zanata/action/ReindexAction.java b/server/zanata-war/src/main/java/org/zanata/action/ReindexAction.java index cf17f9c8ba..a0a0c7aed9 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/ReindexAction.java +++ b/server/zanata-war/src/main/java/org/zanata/action/ReindexAction.java @@ -148,7 +148,7 @@ public boolean isError() { return false; } - public int getReindexCount() { + public long getReindexCount() { if (searchIndexManager.getProcessHandle() == null) { return 0; } else { @@ -156,7 +156,7 @@ public int getReindexCount() { } } - public int getReindexProgress() { + public long getReindexProgress() { if (searchIndexManager.getProcessHandle() == null) { return 0; } else { diff --git a/server/zanata-war/src/main/java/org/zanata/action/TranslationMemoryAction.java b/server/zanata-war/src/main/java/org/zanata/action/TranslationMemoryAction.java index 7cc68305c7..7660e980dd 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/TranslationMemoryAction.java +++ b/server/zanata-war/src/main/java/org/zanata/action/TranslationMemoryAction.java @@ -21,8 +21,9 @@ package org.zanata.action; import static javax.faces.application.FacesMessage.SEVERITY_ERROR; +import static org.zanata.async.AsyncTaskKey.joinFields; + import java.io.Serializable; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.ExecutionException; @@ -32,6 +33,8 @@ import javax.inject.Inject; import javax.inject.Named; import org.apache.deltaspike.jpa.api.transaction.Transactional; +import org.zanata.async.AsyncTaskKey; +import org.zanata.async.GenericAsyncTaskKey; import org.zanata.security.annotations.CheckRole; import org.zanata.async.AsyncTaskHandle; import org.zanata.async.AsyncTaskHandleManager; @@ -58,6 +61,7 @@ public class TranslationMemoryAction implements Serializable { org.slf4j.LoggerFactory.getLogger(TranslationMemoryAction.class); private static final long serialVersionUID = -6791743907133760028L; + private static final String KEY_NAME = "ClearTMXKey"; @Inject private FacesMessages facesMessages; @Inject @@ -69,7 +73,7 @@ public class TranslationMemoryAction implements Serializable { justification = "CDI proxies are Serializable") private AsyncTaskHandleManager asyncTaskHandleManager; private List transMemoryList; - private ClearTransMemoryProcessKey lastTaskTMKey; + private AsyncTaskKey lastTaskTMKey; private SortingType tmSortingList = new SortingType( Lists.newArrayList(SortingType.SortOption.ALPHABETICAL, SortingType.SortOption.CREATED_DATE)); @@ -89,11 +93,11 @@ public List getAllTranslationMemories() { } public void sortTMList() { - Collections.sort(transMemoryList, tmComparator); + transMemoryList.sort(tmComparator); } public void clearTransMemory(final String transMemorySlug) { - lastTaskTMKey = new ClearTransMemoryProcessKey(transMemorySlug); + lastTaskTMKey = new GenericAsyncTaskKey(joinFields(KEY_NAME, transMemorySlug)); AsyncTaskHandle handle = new AsyncTaskHandle(); asyncTaskHandleManager.registerTaskHandle(handle, lastTaskTMKey); translationMemoryResource @@ -159,8 +163,11 @@ public void deleteTransMemory(String transMemorySlug) { } public boolean isTransMemoryBeingCleared(String transMemorySlug) { + GenericAsyncTaskKey taskKey = + new GenericAsyncTaskKey( + joinFields(KEY_NAME, transMemorySlug)); AsyncTaskHandle handle = asyncTaskHandleManager.getHandleByKey( - new ClearTransMemoryProcessKey(transMemorySlug)); + taskKey); return handle != null && !handle.isDone(); } @@ -192,52 +199,6 @@ public String cancel() { return "cancel"; } - /** - * Represents a key to index a translation memory clear process. - * - * NB: Eventually this class might need to live outside if there are other - * services that need to control this process. - */ - private static class ClearTransMemoryProcessKey implements Serializable { - private String slug; - - @java.beans.ConstructorProperties({ "slug" }) - public ClearTransMemoryProcessKey(final String slug) { - this.slug = slug; - } - - @Override - public boolean equals(final Object o) { - if (o == this) - return true; - if (!(o instanceof TranslationMemoryAction.ClearTransMemoryProcessKey)) - return false; - final ClearTransMemoryProcessKey other = - (ClearTransMemoryProcessKey) o; - if (!other.canEqual((Object) this)) - return false; - final Object this$slug = this.slug; - final Object other$slug = other.slug; - if (this$slug == null ? other$slug != null - : !this$slug.equals(other$slug)) - return false; - return true; - } - - protected boolean canEqual(final Object other) { - return other instanceof TranslationMemoryAction.ClearTransMemoryProcessKey; - } - - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - final Object $slug = this.slug; - result = result * PRIME + ($slug == null ? 43 : $slug.hashCode()); - return result; - } - } - private static class TMComparator implements Comparator, Serializable { private SortingType sortingType; diff --git a/server/zanata-war/src/main/java/org/zanata/action/VersionGroupHome.java b/server/zanata-war/src/main/java/org/zanata/action/VersionGroupHome.java index 6af3e22c07..f1110bf5ed 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/VersionGroupHome.java +++ b/server/zanata-war/src/main/java/org/zanata/action/VersionGroupHome.java @@ -60,6 +60,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; +import org.zanata.util.UrlUtil; /** * @author Alex Eng aeng@redhat.com @@ -94,6 +95,8 @@ public class VersionGroupHome extends SlugHome private VersionAutocomplete versionAutocomplete; @Inject private GroupLocaleAutocomplete localeAutocomplete; + @Inject + private UrlUtil urlUtil; private AbstractListFilter maintainerFilter = new InMemoryListFilter() { @@ -223,10 +226,14 @@ public void removeMaintainer(HPerson maintainer) { } else { getInstance().removeMaintainer(maintainer); maintainerFilter.reset(); - update(); + super.update(); conversationScopeMessages.setMessage(FacesMessage.SEVERITY_INFO, msgs.format("jsf.MaintainerRemoveFromGroup", maintainer.getName())); + if (maintainer.equals(authenticatedAccount.getPerson())) { + urlUtil.redirectToInternal( + urlUtil.groupUrl(getInstance().getSlug())); + } } } diff --git a/server/zanata-war/src/main/java/org/zanata/action/VersionHome.java b/server/zanata-war/src/main/java/org/zanata/action/VersionHome.java index 42abb0e932..c4627c5f3a 100644 --- a/server/zanata-war/src/main/java/org/zanata/action/VersionHome.java +++ b/server/zanata-war/src/main/java/org/zanata/action/VersionHome.java @@ -79,6 +79,7 @@ import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import java.util.stream.Collectors; @Named("versionHome") @ViewScoped @@ -946,16 +947,14 @@ public List getDisabledLocales() { * already in the project. */ private List findActiveNotEnabledLocales() { - Collection filtered = Collections2 - .filter(localeDAO.findAllActive(), new Predicate() { - - @Override - public boolean apply(HLocale input) { - // only include those not already in the project - return !getEnabledLocales().contains(input); - } - }); - return Lists.newArrayList(filtered); + List activeLocales = localeDAO.findAllActive(); + // only include those not already in the project version + List filteredList = activeLocales.stream() + .filter(hLocale -> !getEnabledLocales().contains(hLocale)) + .collect( + Collectors.toList()); + Collections.sort(filteredList, ComparatorUtil.LOCALE_COMPARATOR); + return filteredList; } private Map selectedDisabledLocales = Maps.newHashMap(); diff --git a/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandle.java b/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandle.java index f4d5c47d8e..9247229109 100644 --- a/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandle.java +++ b/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandle.java @@ -21,14 +21,17 @@ package org.zanata.async; import java.io.Serializable; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; +import org.zanata.security.ZanataIdentity; + import com.google.common.base.Optional; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** @@ -42,18 +45,24 @@ public class AsyncTaskHandle implements Serializable { @SuppressFBWarnings("SE_BAD_FIELD") private CompletableFuture futureResult; - public int maxProgress = 100; - public int minProgress = 0; - public int currentProgress = 0; + public long maxProgress = 100; + public long currentProgress = 0; private long startTime = -1; private long finishTime = -1; + private String cancelledBy; + private long cancelledTime; + private String keyId; + + public boolean isRunning() { + return isStarted() && !isCancelled() && !isDone(); + } - public int increaseProgress(int increaseBy) { + public long increaseProgress(long increaseBy) { currentProgress += increaseBy; return currentProgress; } - void startTiming() { + protected void startTiming() { startTime = System.currentTimeMillis(); } @@ -95,7 +104,7 @@ public Optional getEstimatedTimeRemaining() { if (this.startTime > 0 && currentProgress > 0) { long currentTime = System.currentTimeMillis(); long timeElapsed = currentTime - this.startTime; - int remainingUnits = this.maxProgress - this.currentProgress; + long remainingUnits = this.maxProgress - this.currentProgress; return Optional .of(timeElapsed * remainingUnits / this.currentProgress); } else { @@ -103,6 +112,27 @@ public Optional getEstimatedTimeRemaining() { } } + public boolean isVisibleTo(ZanataIdentity identity) { + return isAdminOrSameUser(this, identity); + } + + public boolean canCancel(ZanataIdentity identity) { + return isAdminOrSameUser(this, identity); + } + + private static boolean isAdminOrSameUser(AsyncTaskHandle taskHandle, + ZanataIdentity identity) { + return identity != null && (identity.hasRole("admin") + || triggeredBySameUser(taskHandle, identity)); + } + + private static boolean triggeredBySameUser(AsyncTaskHandle taskHandle, + ZanataIdentity identity) { + return taskHandle instanceof UserTriggeredTaskHandle && Objects.equals( + ((UserTriggeredTaskHandle) taskHandle).getTriggeredBy(), + identity.getAccountUsername()); + } + /** * @return The time that the task has been executing for, or the total * execution time if the task has finished (in milliseconds). @@ -147,31 +177,23 @@ public long getTimeSinceFinish() { } } - void setFutureResult(final CompletableFuture futureResult) { + protected void setFutureResult(final CompletableFuture futureResult) { this.futureResult = futureResult; } - public int getMaxProgress() { + public long getMaxProgress() { return this.maxProgress; } - public void setMaxProgress(final int maxProgress) { + public void setMaxProgress(final long maxProgress) { this.maxProgress = maxProgress; } - public int getMinProgress() { - return this.minProgress; - } - - public void setMinProgress(final int minProgress) { - this.minProgress = minProgress; - } - - public int getCurrentProgress() { + public long getCurrentProgress() { return this.currentProgress; } - public void setCurrentProgress(final int currentProgress) { + protected void setCurrentProgress(final long currentProgress) { this.currentProgress = currentProgress; } @@ -195,4 +217,28 @@ public long getFinishTime() { public void whenTaskComplete(BiConsumer action) { futureResult = futureResult.whenComplete(action); } + + public String getCancelledBy() { + return this.cancelledBy; + } + + public void setCancelledBy(final String cancelledBy) { + this.cancelledBy = cancelledBy; + } + + public long getCancelledTime() { + return this.cancelledTime; + } + + public void setCancelledTime(final long cancelledTime) { + this.cancelledTime = cancelledTime; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } } diff --git a/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandleManager.java b/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandleManager.java index b91d0ccd87..94b22eb17b 100644 --- a/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandleManager.java +++ b/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskHandleManager.java @@ -23,15 +23,17 @@ import java.io.Serializable; import java.util.Collection; import java.util.Map; -import java.util.UUID; import java.util.concurrent.TimeUnit; -import com.google.common.collect.Lists; import javax.inject.Named; +import com.google.common.base.Preconditions; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** @@ -40,40 +42,39 @@ @Named("asyncTaskHandleManager") @javax.enterprise.context.ApplicationScoped public class AsyncTaskHandleManager implements Serializable { - + private static final long serialVersionUID = -3209755141964141830L; @SuppressFBWarnings(value = "SE_BAD_FIELD") - private Map handlesByKey = Maps + private final Map> handlesByKey = Maps .newConcurrentMap(); // Cache of recently completed tasks - private Cache finishedTasks = CacheBuilder + private Cache> finishedTasks = CacheBuilder .newBuilder().expireAfterWrite(10, TimeUnit.MINUTES) .build(); - public synchronized void registerTaskHandle(AsyncTaskHandle handle, - Serializable key) { - if (handlesByKey.containsKey(key)) { - throw new RuntimeException("Task handle with key " + key - + " already exists"); - } - handlesByKey.put(key, handle); + public synchronized void registerTaskHandle(AsyncTaskHandle handle, + K key) { + AsyncTaskHandle existingHandle = + handlesByKey.putIfAbsent(key.id(), handle); + Preconditions.checkArgument(existingHandle == null, + "Task handle with key " + key + " already exists"); } /** * Registers a task handle. * @param handle The handle to register. - * @return An auto generated key to retreive the handle later + * @return An auto generated key id to retrieve the handle later */ - public synchronized Serializable registerTaskHandle(AsyncTaskHandle handle) { - Serializable autoGenKey = UUID.randomUUID().toString(); - registerTaskHandle(handle, autoGenKey); - return autoGenKey; + public synchronized String registerTaskHandle(AsyncTaskHandle handle) { + GenericAsyncTaskKey genericKey = new GenericAsyncTaskKey(); + registerTaskHandle(handle, genericKey); + return genericKey.id(); } void taskFinished(AsyncTaskHandle taskHandle) { synchronized (handlesByKey) { // TODO This operation is O(n). Maybe we can do better? - for (Map.Entry entry : handlesByKey + for (Map.Entry> entry : handlesByKey .entrySet()) { if (entry.getValue().equals(taskHandle)) { handlesByKey.remove(entry.getKey()); @@ -83,11 +84,16 @@ void taskFinished(AsyncTaskHandle taskHandle) { } } - public AsyncTaskHandle getHandleByKey(Serializable key) { - if (handlesByKey.containsKey(key)) { - return handlesByKey.get(key); + public AsyncTaskHandle getHandleByKey(K key) { + return getHandleByKeyId(key.id()); + } + + @SuppressWarnings("unchecked") + public AsyncTaskHandle getHandleByKeyId(String keyId) { + if (handlesByKey.containsKey(keyId)) { + return (AsyncTaskHandle) handlesByKey.get(keyId); } - return finishedTasks.getIfPresent(key); + return (AsyncTaskHandle) finishedTasks.getIfPresent(keyId); } public Collection getAllHandles() { @@ -96,4 +102,16 @@ public Collection getAllHandles() { handles.addAll(finishedTasks.asMap().values()); return handles; } + + public Map> getAllTasks() { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + builder.putAll(handlesByKey); + builder.putAll(finishedTasks.asMap()); + return builder.build(); + } + + public Map> getRunningTasks() { + return ImmutableMap.copyOf(handlesByKey); + } + } diff --git a/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskKey.java b/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskKey.java new file mode 100644 index 0000000000..9a92eb3596 --- /dev/null +++ b/server/zanata-war/src/main/java/org/zanata/async/AsyncTaskKey.java @@ -0,0 +1,35 @@ +package org.zanata.async; + +import java.io.Serializable; + +import com.google.common.base.Joiner; + +/** + * @author Patrick Huang pahuang@redhat.com + */ +public interface AsyncTaskKey extends Serializable { + /** + * When converting multiple fields to form id string, we + * should use this as separator (URL friendly). + */ + String SEPARATOR = "-"; + + /** + * @return a unique identifier for the key + */ + String id(); + + /** + * Helper method to convert list of fields to a String as key id. + * + * @param keyName + * the name for this key + * @param fields + * key instance field values + * @return String representation of the key which can be used as id + */ + static String joinFields(String keyName, Object... fields) { + return keyName + SEPARATOR + + Joiner.on(SEPARATOR).useForNull("").join(fields); + } +} diff --git a/server/zanata-war/src/main/java/org/zanata/async/GenericAsyncTaskKey.java b/server/zanata-war/src/main/java/org/zanata/async/GenericAsyncTaskKey.java new file mode 100644 index 0000000000..8c730ccfc7 --- /dev/null +++ b/server/zanata-war/src/main/java/org/zanata/async/GenericAsyncTaskKey.java @@ -0,0 +1,47 @@ +package org.zanata.async; + +import java.util.Objects; +import java.util.UUID; + +import com.google.common.base.MoreObjects; + +/** + * @author Patrick Huang pahuang@redhat.com + */ +public class GenericAsyncTaskKey implements AsyncTaskKey { + private static final long serialVersionUID = -8648519833116851231L; + private final String id; + + GenericAsyncTaskKey() { + id = UUID.randomUUID().toString(); + } + + public GenericAsyncTaskKey(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GenericAsyncTaskKey that = (GenericAsyncTaskKey) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/server/zanata-war/src/main/java/org/zanata/async/UserTriggeredTaskHandle.java b/server/zanata-war/src/main/java/org/zanata/async/UserTriggeredTaskHandle.java new file mode 100644 index 0000000000..4bf0d79d26 --- /dev/null +++ b/server/zanata-war/src/main/java/org/zanata/async/UserTriggeredTaskHandle.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.zanata.async; + + +/** + * Represents a task triggered by a user (as oppose to some system generated + * async tasks). For such tasks, the user takes ownership of this task and is + * allowed to cancel the task. Admin user can cancel any tasks. + */ +public interface UserTriggeredTaskHandle { + void setTriggeredBy(String username); + + String getTriggeredBy(); + +} diff --git a/server/zanata-war/src/main/java/org/zanata/async/handle/CopyTransTaskHandle.java b/server/zanata-war/src/main/java/org/zanata/async/handle/CopyTransTaskHandle.java index 87422a836f..64f7538588 100644 --- a/server/zanata-war/src/main/java/org/zanata/async/handle/CopyTransTaskHandle.java +++ b/server/zanata-war/src/main/java/org/zanata/async/handle/CopyTransTaskHandle.java @@ -21,15 +21,15 @@ package org.zanata.async.handle; import org.zanata.async.AsyncTaskHandle; +import org.zanata.async.UserTriggeredTaskHandle; /** * @author Carlos Munoz * camunoz@redhat.com */ -public class CopyTransTaskHandle extends AsyncTaskHandle { +public class CopyTransTaskHandle extends AsyncTaskHandle implements + UserTriggeredTaskHandle { - private String cancelledBy; - private long cancelledTime; private String triggeredBy; private boolean prepared; @@ -37,26 +37,12 @@ public void setPrepared() { this.prepared = true; } - public String getCancelledBy() { - return this.cancelledBy; - } - - public void setCancelledBy(final String cancelledBy) { - this.cancelledBy = cancelledBy; - } - - public long getCancelledTime() { - return this.cancelledTime; - } - - public void setCancelledTime(final long cancelledTime) { - this.cancelledTime = cancelledTime; - } - + @Override public String getTriggeredBy() { return this.triggeredBy; } + @Override public void setTriggeredBy(final String triggeredBy) { this.triggeredBy = triggeredBy; } diff --git a/server/zanata-war/src/main/java/org/zanata/async/handle/CopyVersionTaskHandle.java b/server/zanata-war/src/main/java/org/zanata/async/handle/CopyVersionTaskHandle.java index 0bf388f6f7..2d1b0882d1 100644 --- a/server/zanata-war/src/main/java/org/zanata/async/handle/CopyVersionTaskHandle.java +++ b/server/zanata-war/src/main/java/org/zanata/async/handle/CopyVersionTaskHandle.java @@ -21,6 +21,7 @@ package org.zanata.async.handle; import org.zanata.async.AsyncTaskHandle; +import org.zanata.async.UserTriggeredTaskHandle; /** * Asynchronous task handle for the copy version process. @@ -28,12 +29,11 @@ * @author Carlos Munoz * camunoz@redhat.com */ -public class CopyVersionTaskHandle extends AsyncTaskHandle { +public class CopyVersionTaskHandle extends AsyncTaskHandle implements + UserTriggeredTaskHandle { private int documentCopied; private int totalDoc; - private String cancelledBy; - private long cancelledTime; private String triggeredBy; /** @@ -59,26 +59,12 @@ public void setTotalDoc(final int totalDoc) { this.totalDoc = totalDoc; } - public String getCancelledBy() { - return this.cancelledBy; - } - - public void setCancelledBy(final String cancelledBy) { - this.cancelledBy = cancelledBy; - } - - public long getCancelledTime() { - return this.cancelledTime; - } - - public void setCancelledTime(final long cancelledTime) { - this.cancelledTime = cancelledTime; - } - + @Override public String getTriggeredBy() { return this.triggeredBy; } + @Override public void setTriggeredBy(final String triggeredBy) { this.triggeredBy = triggeredBy; } diff --git a/server/zanata-war/src/main/java/org/zanata/async/handle/MergeTranslationsTaskHandle.java b/server/zanata-war/src/main/java/org/zanata/async/handle/MergeTranslationsTaskHandle.java index ec8274470c..585672721e 100644 --- a/server/zanata-war/src/main/java/org/zanata/async/handle/MergeTranslationsTaskHandle.java +++ b/server/zanata-war/src/main/java/org/zanata/async/handle/MergeTranslationsTaskHandle.java @@ -21,47 +21,40 @@ package org.zanata.async.handle; import org.zanata.async.AsyncTaskHandle; +import org.zanata.async.AsyncTaskKey; +import org.zanata.async.UserTriggeredTaskHandle; /** * Asynchronous task handle for the merge translations process. * * @author Alex Eng aeng@redhat.com */ -public class MergeTranslationsTaskHandle extends AsyncTaskHandle { +public class MergeTranslationsTaskHandle extends AsyncTaskHandle implements + UserTriggeredTaskHandle { - private int totalTranslations; - private String cancelledBy; - private long cancelledTime; + private static final long serialVersionUID = -8026264371441200919L; + private long totalTranslations; private String triggeredBy; - public int getTotalTranslations() { - return this.totalTranslations; - } - - public void setTotalTranslations(final int totalTranslations) { - this.totalTranslations = totalTranslations; - } - - public String getCancelledBy() { - return this.cancelledBy; - } - - public void setCancelledBy(final String cancelledBy) { - this.cancelledBy = cancelledBy; + public MergeTranslationsTaskHandle( + AsyncTaskKey key) { + setKeyId(key.id()); } - public long getCancelledTime() { - return this.cancelledTime; + public long getTotalTranslations() { + return this.totalTranslations; } - public void setCancelledTime(final long cancelledTime) { - this.cancelledTime = cancelledTime; + public void setTotalTranslations(final long totalTranslations) { + this.totalTranslations = totalTranslations; } + @Override public String getTriggeredBy() { return this.triggeredBy; } + @Override public void setTriggeredBy(final String triggeredBy) { this.triggeredBy = triggeredBy; } diff --git a/server/zanata-war/src/main/java/org/zanata/async/handle/TransMemoryMergeTaskHandle.java b/server/zanata-war/src/main/java/org/zanata/async/handle/TransMemoryMergeTaskHandle.java index aa5421fa0e..71e566794c 100644 --- a/server/zanata-war/src/main/java/org/zanata/async/handle/TransMemoryMergeTaskHandle.java +++ b/server/zanata-war/src/main/java/org/zanata/async/handle/TransMemoryMergeTaskHandle.java @@ -21,6 +21,7 @@ package org.zanata.async.handle; import org.zanata.async.AsyncTaskHandle; +import org.zanata.async.UserTriggeredTaskHandle; import org.zanata.common.LocaleId; import org.zanata.webtrans.shared.model.DocumentId; import org.zanata.webtrans.shared.model.ProjectIterationId; @@ -30,50 +31,17 @@ /** * @author Patrick Huang pahuang@redhat.com */ -public class TransMemoryMergeTaskHandle extends AsyncTaskHandle { - private long textFlowFilled; - private long totalTextFlows; - private String cancelledBy; - private Long cancelledTime; +public class TransMemoryMergeTaskHandle extends AsyncTaskHandle implements + UserTriggeredTaskHandle { private String triggeredBy; private String mergeTarget; - public long getTextFlowFilled() { - return this.textFlowFilled; - } - - public void setTextFlowFilled(final long textFlowFilled) { - this.textFlowFilled = textFlowFilled; - } - - public long getTotalTextFlows() { - return this.totalTextFlows; - } - - public void setTotalTextFlows(final long totalTextFlows) { - this.totalTextFlows = totalTextFlows; - } - - public String getCancelledBy() { - return this.cancelledBy; - } - - public void setCancelledBy(final String cancelledBy) { - this.cancelledBy = cancelledBy; - } - - public long getCancelledTime() { - return this.cancelledTime; - } - - public void setCancelledTime(final long cancelledTime) { - this.cancelledTime = cancelledTime; - } - + @Override public String getTriggeredBy() { return this.triggeredBy; } + @Override public void setTriggeredBy(final String triggeredBy) { this.triggeredBy = triggeredBy; } @@ -84,19 +52,13 @@ public void setTMMergeTarget(ProjectIterationId projectIterationId, documentId.getDocId(), localeId); } - public String getMergeTarget() { - return mergeTarget; - } - @Override public String toString() { return MoreObjects.toStringHelper(this) .omitNullValues() .add("mergeTarget", mergeTarget) - .add("textFlowFilled", textFlowFilled) - .add("totalTextFlows", totalTextFlows) - .add("cancelledBy", cancelledBy) - .add("cancelledTime", cancelledTime) + .add("currentProgress", currentProgress) + .add("maxProgress", maxProgress) .add("triggeredBy", triggeredBy) .toString(); } diff --git a/server/zanata-war/src/main/java/org/zanata/dao/GenericDAO.java b/server/zanata-war/src/main/java/org/zanata/dao/GenericDAO.java index 7043c386d9..74ffe3588a 100644 --- a/server/zanata-war/src/main/java/org/zanata/dao/GenericDAO.java +++ b/server/zanata-war/src/main/java/org/zanata/dao/GenericDAO.java @@ -8,6 +8,8 @@ */ public interface GenericDAO { + int MAX_PARAMS_IN_IN_CLAUSE = 50; + T findById(ID id, boolean lock); void deleteAll(); diff --git a/server/zanata-war/src/main/java/org/zanata/dao/ProjectDAO.java b/server/zanata-war/src/main/java/org/zanata/dao/ProjectDAO.java index 8186dd3592..df2ea80bea 100644 --- a/server/zanata-war/src/main/java/org/zanata/dao/ProjectDAO.java +++ b/server/zanata-war/src/main/java/org/zanata/dao/ProjectDAO.java @@ -50,6 +50,8 @@ import org.zanata.model.HProjectIteration; import org.zanata.model.ProjectRole; +import static org.zanata.hibernate.search.IndexFieldLabels.FULL_SLUG_FIELD; + @RequestScoped public class ProjectDAO extends AbstractDAOImpl { @Inject @FullText @@ -63,6 +65,11 @@ public ProjectDAO(Session session) { super(HProject.class, session); } + public ProjectDAO(FullTextEntityManager entityManager, Session session) { + super(HProject.class, session); + this.entityManager = entityManager; + } + public @Nullable HProject getBySlug(@Nonnull String slug) { if (!StringUtils.isEmpty(slug)) { @@ -280,13 +287,13 @@ public int getQueryProjectSize(@Nonnull String searchQuery, private FullTextQuery buildSearchQuery(@Nonnull String searchQuery, boolean includeObsolete) throws ParseException { - String queryText = QueryParser.escape(searchQuery); - - BooleanQuery booleanQuery = new BooleanQuery(); - booleanQuery.add(buildSearchFieldQuery(queryText, "slug"), BooleanClause.Occur.SHOULD); - booleanQuery.add(buildSearchFieldQuery(queryText, "name"), BooleanClause.Occur.SHOULD); - booleanQuery.add(buildSearchFieldQuery(queryText, "description"), BooleanClause.Occur.SHOULD); + BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder(); + // for slug, we do prefix search (so people can search with '-' in it + booleanQuery.add(buildSearchFieldQuery(searchQuery, FULL_SLUG_FIELD, true), BooleanClause.Occur.SHOULD); + // for name and description, we split the word using the same analyzer and search as is + booleanQuery.add(buildSearchFieldQuery(searchQuery, "name", false), BooleanClause.Occur.SHOULD); + booleanQuery.add(buildSearchFieldQuery(searchQuery, "description", false), BooleanClause.Occur.SHOULD); if (!includeObsolete) { TermQuery obsoleteStateQuery = new TermQuery(new Term(IndexFieldLabels.ENTITY_STATUS, @@ -294,7 +301,8 @@ private FullTextQuery buildSearchQuery(@Nonnull String searchQuery, booleanQuery.add(obsoleteStateQuery, BooleanClause.Occur.MUST_NOT); } - return entityManager.createFullTextQuery(booleanQuery, HProject.class); + BooleanQuery luceneQuery = booleanQuery.build(); + return entityManager.createFullTextQuery(luceneQuery, HProject.class); } /** @@ -302,24 +310,26 @@ private FullTextQuery buildSearchQuery(@Nonnull String searchQuery, * white space. * * @param searchQuery - * - query string, will replace hypen with space and escape - * special char + * - query string, will escape special char * @param field * - lucene field + * @param wildcard + * - whether append wildcard to the end */ private BooleanQuery buildSearchFieldQuery(@Nonnull String searchQuery, - @Nonnull String field) throws ParseException { - BooleanQuery query = new BooleanQuery(); - - //escape special character search - searchQuery = QueryParser.escape(searchQuery); + @Nonnull String field, boolean wildcard) throws ParseException { + BooleanQuery.Builder query = new BooleanQuery.Builder(); for(String searchString: searchQuery.split("\\s+")) { QueryParser parser = new QueryParser(field, new CaseInsensitiveWhitespaceAnalyzer()); - query.add(parser.parse(searchString + "*"), BooleanClause.Occur.MUST); + //escape special character search + + String escaped = QueryParser.escape(searchString); + escaped = wildcard ? escaped + "*" : escaped; + query.add(parser.parse(escaped), BooleanClause.Occur.MUST); } - return query; + return query.build(); } public List findAllTranslatedProjects(HAccount account, int maxResults) { diff --git a/server/zanata-war/src/main/java/org/zanata/dao/ProjectIterationDAO.java b/server/zanata-war/src/main/java/org/zanata/dao/ProjectIterationDAO.java index 62ef48af14..e5f00f2923 100644 --- a/server/zanata-war/src/main/java/org/zanata/dao/ProjectIterationDAO.java +++ b/server/zanata-war/src/main/java/org/zanata/dao/ProjectIterationDAO.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -50,6 +51,7 @@ import org.zanata.util.HashUtil; import org.zanata.util.StatisticsUtil; +import com.google.common.base.Preconditions; import com.google.common.collect.Lists; @RequestScoped @@ -499,23 +501,23 @@ public List searchByProjectId(Long projectId) { return q.list(); } - public List searchByProjectIdExcludingStatus( - Long projectId, EntityStatus... exclude) { - StringBuilder sb = new StringBuilder(); - sb.append("FROM HProjectIteration t WHERE t.project.id = :projectId "); - for (EntityStatus status : exclude) { - sb.append("AND t.status != :"); - sb.append(status.toString()); - } - sb.append(" order by t.creationDate DESC"); - Query q = getSession().createQuery(sb.toString()); - q.setParameter("projectId", projectId); - - for (EntityStatus status : exclude) { - q.setParameter(status.toString(), status); - } - q.setCacheable(false).setComment( - "ProjectIterationDAO.searchByProjectIdExcludeObsolete"); + public List searchByProjectsExcludeObsolete( + List projects) { + Preconditions.checkArgument( + !projects.isEmpty() + && projects.size() < MAX_PARAMS_IN_IN_CLAUSE, + "invalid projectIds list"); + String query = + "FROM HProjectIteration t WHERE t.project.id in (:projectIds) " + + "AND t.status != :status" + + " order by t.project.id, t.creationDate DESC"; + List projectIds = projects.stream().map(HProject::getId).collect( + Collectors.toList()); + Query q = getSession().createQuery(query) + .setParameterList("projectIds", projectIds) + .setParameter("status", EntityStatus.OBSOLETE) + .setCacheable(false).setComment( + "ProjectIterationDAO.searchByProjectIdsExcludeObsolete"); return q.list(); } diff --git a/server/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java b/server/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java index a09ab117fa..e262e1ddbd 100644 --- a/server/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java +++ b/server/zanata-war/src/main/java/org/zanata/dao/TextFlowDAO.java @@ -310,6 +310,41 @@ public int getSourceByMatchedContextCount(Long sourceVersionId, return count == null ? 0 : count.intValue(); } + public long getUntranslatedOrFuzzyTextFlowCountInVersion(Long targetVersionId, HLocale targetLocale) { + String queryString = + "select count(*) from HTextFlow tf left join " + + "tf.targets tfts WITH index(tfts) =:locale" + + " WHERE (tf.obsolete=0 AND tf.document.projectIteration.id=:targetVersionId ) AND" + + " ( EXISTS ( FROM HTextFlowTarget WHERE ((textFlow=tf AND locale.id=:locale) AND state = :untranslatedState)) OR (:locale not in indices(tf.targets) ))"; + Query query = getSession().createQuery(queryString) + .setParameter("locale", targetLocale.getId()) + .setParameter("targetVersionId", targetVersionId) + .setParameter("untranslatedState", ContentState.New) + .setCacheable(true) + .setComment("TextFlowDAO.getUntranslatedOrFuzzyTextFlowCountInVersion"); + Long count = (Long) query.uniqueResult(); + return count == null ? 0 : count; + } + + public List getUntranslatedOrFuzzyTextFlowsInVersion( + Long targetVersionId, + HLocale targetLocale, int startIndex, int maxCount) { + String queryString = + "select distinct tf from HTextFlow tf left join " + + "tf.targets tfts WITH index(tfts) =:locale" + + " WHERE (tf.obsolete=0 AND tf.document.projectIteration.id=:targetVersionId ) AND" + + " ( EXISTS ( FROM HTextFlowTarget WHERE ((textFlow=tf AND locale.id=:locale) AND state = :untranslatedState)) OR (:locale not in indices(tf.targets) ))"; + + Query query = getSession().createQuery(queryString) + .setParameter("locale", targetLocale.getId()) + .setParameter("targetVersionId", targetVersionId) + .setParameter("untranslatedState", ContentState.New) + .setFirstResult(startIndex).setMaxResults(maxCount) + .setCacheable(true) + .setComment("TextFlowDAO.getUntranslatedOrFuzzyTextFlowsInVersion"); + return query.list(); + } + /** * Generate query string for text flows that have matching document id and * content between the given source and target version diff --git a/server/zanata-war/src/main/java/org/zanata/dao/TextFlowStreamingDAO.java b/server/zanata-war/src/main/java/org/zanata/dao/TextFlowStreamingDAO.java index 48c8e7ada5..4a83ea6836 100644 --- a/server/zanata-war/src/main/java/org/zanata/dao/TextFlowStreamingDAO.java +++ b/server/zanata-war/src/main/java/org/zanata/dao/TextFlowStreamingDAO.java @@ -30,12 +30,15 @@ import javax.inject.Inject; import javax.inject.Named; import org.zanata.common.EntityStatus; +import org.zanata.common.LocaleId; import org.zanata.model.HProject; import org.zanata.model.HProjectIteration; import org.zanata.model.HTextFlow; import org.zanata.util.CloseableIterator; import org.zanata.util.Zanata; +import java.util.Optional; + /** * This class uses Hibernate's StatelessSession to iterate over large queries * returning HTextFlow. Each of the public methods should have a variant which @@ -74,24 +77,30 @@ public TextFlowStreamingDAO(@Zanata SessionFactory sessionFactory) { * @return */ public @Nonnull - CloseableIterator findTextFlows() { + CloseableIterator findTextFlows(Optional localeId) { StreamingEntityIterator iter = createIterator(); try { - Query q = - iter.getSession() - .createQuery( - "from HTextFlow tf " - + "inner join fetch tf.targets target " - + "inner join fetch target.locale " - + "inner join fetch tf.document " - + "inner join fetch tf.document.locale " - + "inner join fetch tf.document.projectIteration " - + "inner join fetch tf.document.projectIteration.project " - + "where tf.document.projectIteration.project.status<>:OBSOLETE " - + "and tf.document.projectIteration.status<>:OBSOLETE " - + "and tf.document.obsolete=0 " - + "and tf.obsolete=0 "); + StringBuilder queryString = new StringBuilder(); + queryString + .append("from HTextFlow tf ") + .append("inner join fetch tf.targets target ") + .append("inner join fetch target.locale ") + .append("inner join fetch tf.document ") + .append("inner join fetch tf.document.locale ") + .append("inner join fetch tf.document.projectIteration ") + .append("inner join fetch tf.document.projectIteration.project ") + .append("where tf.document.projectIteration.project.status<>:OBSOLETE ") + .append("and tf.document.projectIteration.status<>:OBSOLETE ") + .append("and tf.document.obsolete=0 ") + .append("and tf.obsolete=0 "); + if (localeId.isPresent()) { + queryString.append("and tf.document.locale.localeId=:localeId"); + } + Query q = iter.getSession().createQuery(queryString.toString()); q.setParameter("OBSOLETE", EntityStatus.OBSOLETE); + if (localeId.isPresent()) { + q.setParameter("localeId", localeId.get()); + } q.setComment("TextFlowStreamDAO.findTextFlows"); iter.initQuery(q); return iter; @@ -113,25 +122,31 @@ CloseableIterator findTextFlows() { */ public @Nonnull CloseableIterator - findTextFlowsByProject(HProject hProject) { + findTextFlowsByProject(HProject hProject, Optional localeId) { StreamingEntityIterator iter = createIterator(); try { - Query q = - iter.getSession() - .createQuery( - "from HTextFlow tf " - + "inner join fetch tf.targets target " - + "inner join fetch target.locale " - + "inner join fetch tf.document " - + "inner join fetch tf.document.locale " - + "inner join fetch tf.document.projectIteration " - + "inner join fetch tf.document.projectIteration.project " - + "where tf.document.projectIteration.status<>:OBSOLETE " - + "and tf.document.obsolete=0 " - + "and tf.obsolete=0" - + "and tf.document.projectIteration.project=:proj"); + StringBuilder queryString = new StringBuilder(); + queryString + .append("from HTextFlow tf ") + .append("inner join fetch tf.targets target ") + .append("inner join fetch target.locale ") + .append("inner join fetch tf.document ") + .append("inner join fetch tf.document.locale ") + .append("inner join fetch tf.document.projectIteration ") + .append("inner join fetch tf.document.projectIteration.project ") + .append("where tf.document.projectIteration.status<>:OBSOLETE ") + .append("and tf.document.obsolete=0 ") + .append("and tf.obsolete=0 ") + .append("and tf.document.projectIteration.project=:proj "); + if (localeId.isPresent()) { + queryString.append("and tf.document.locale.localeId=:localeId"); + } + Query q = iter.getSession().createQuery(queryString.toString()); q.setParameter("OBSOLETE", EntityStatus.OBSOLETE); q.setParameter("proj", hProject); + if (localeId.isPresent()) { + q.setParameter("localeId", localeId.get()); + } q.setComment("TextFlowStreamDAO.findTextFlowsByProject"); iter.initQuery(q); return iter; @@ -152,24 +167,30 @@ CloseableIterator findTextFlows() { * @return */ public @Nonnull - CloseableIterator findTextFlowsByProjectIteration( - HProjectIteration hProjectIteration) { + CloseableIterator findTextFlowsByProjectIteration( + HProjectIteration hProjectIteration, Optional localeId) { StreamingEntityIterator iter = createIterator(); try { - Query q = - iter.getSession() - .createQuery( - "from HTextFlow tf " - + "inner join fetch tf.targets target " - + "inner join fetch target.locale " - + "inner join fetch tf.document " - + "inner join fetch tf.document.locale " - + "inner join fetch tf.document.projectIteration " - + "inner join fetch tf.document.projectIteration.project " - + "where tf.document.obsolete=0 " - + "and tf.obsolete=0" - + "and tf.document.projectIteration=:iter"); + StringBuilder queryString = new StringBuilder(); + queryString + .append("from HTextFlow tf ") + .append("inner join fetch tf.targets target ") + .append("inner join fetch target.locale ") + .append("inner join fetch tf.document ") + .append("inner join fetch tf.document.locale ") + .append("inner join fetch tf.document.projectIteration ") + .append("inner join fetch tf.document.projectIteration.project ") + .append("where tf.document.obsolete=0 ") + .append("and tf.obsolete=0 ") + .append("and tf.document.projectIteration=:iter "); + if (localeId.isPresent()) { + queryString.append("and tf.document.locale.localeId=:localeId"); + } + Query q = iter.getSession().createQuery(queryString.toString()); q.setParameter("iter", hProjectIteration); + if (localeId.isPresent()) { + q.setParameter("localeId", localeId.get()); + } q.setComment("TextFlowStreamDAO.findTextFlowsByProjectIteration"); iter.initQuery(q); diff --git a/server/zanata-war/src/main/java/org/zanata/dao/TransMemoryDAO.java b/server/zanata-war/src/main/java/org/zanata/dao/TransMemoryDAO.java index 0999db4e89..396a74fe12 100644 --- a/server/zanata-war/src/main/java/org/zanata/dao/TransMemoryDAO.java +++ b/server/zanata-war/src/main/java/org/zanata/dao/TransMemoryDAO.java @@ -21,6 +21,7 @@ package org.zanata.dao; import java.util.List; +import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -37,7 +38,6 @@ import org.zanata.jpa.FullText; import org.zanata.model.tm.TransMemory; import org.zanata.model.tm.TransMemoryUnit; -import com.google.common.base.Optional; /** * Data Access Object for Translation Memory and related entities. @@ -68,9 +68,9 @@ public Optional getBySlug(@Nonnull String slug) { TransMemory tm = (TransMemory) getSession().byNaturalId(TransMemory.class) .using("slug", slug).load(); - return Optional.fromNullable(tm); + return Optional.ofNullable(tm); } - return Optional.absent(); + return Optional.empty(); } /** diff --git a/server/zanata-war/src/main/java/org/zanata/rest/dto/VersionTMMerge.java b/server/zanata-war/src/main/java/org/zanata/rest/dto/VersionTMMerge.java new file mode 100644 index 0000000000..d4a4f8d6ae --- /dev/null +++ b/server/zanata-war/src/main/java/org/zanata/rest/dto/VersionTMMerge.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.zanata.rest.dto; + +import java.util.List; + +import org.zanata.common.LocaleId; +import org.zanata.webtrans.shared.model.ProjectIterationId; +import org.zanata.webtrans.shared.rest.dto.HasTMMergeCriteria; +import org.zanata.webtrans.shared.rpc.MergeRule; + +/** + * @author Patrick Huang pahuang@redhat.com + */ +public class VersionTMMerge implements HasTMMergeCriteria { + private LocaleId localeId; + private int thresholdPercent; + private MergeRule differentDocumentRule; + private MergeRule differentContextRule; + private MergeRule importedMatchRule; + private List fromProjectVersions; + + public VersionTMMerge(LocaleId localeId, int thresholdPercent, + MergeRule differentDocumentRule, + MergeRule differentContextRule, + MergeRule importedMatchRule, + List fromProjectVersions) { + this.localeId = localeId; + this.thresholdPercent = thresholdPercent; + this.differentDocumentRule = differentDocumentRule; + this.differentContextRule = differentContextRule; + this.importedMatchRule = importedMatchRule; + this.fromProjectVersions = fromProjectVersions; + } + + @SuppressWarnings("unused") + public VersionTMMerge() { + } + + public LocaleId getLocaleId() { + return localeId; + } + + @Override + public int getThresholdPercent() { + return thresholdPercent; + } + + @Override + public MergeRule getDifferentProjectRule() { + // TM merge for version always accept TM from different project + return MergeRule.FUZZY; + } + + @Override + public MergeRule getDifferentDocumentRule() { + return differentDocumentRule; + } + + @Override + public MergeRule getDifferentContextRule() { + return differentContextRule; + } + + @Override + public MergeRule getImportedMatchRule() { + return importedMatchRule; + } + + public List getFromProjectVersions() { + return fromProjectVersions; + } +} diff --git a/server/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java b/server/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java index 94580ad173..ba411d3b1d 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/editor/service/StatisticsService.java @@ -32,10 +32,10 @@ import org.zanata.common.LocaleId; import org.zanata.dao.DocumentDAO; import org.zanata.model.HDocument; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.stats.ContainerTranslationStatistics; import org.zanata.rest.dto.stats.TranslationStatistics; import org.zanata.rest.dto.stats.TranslationStatistics.StatUnit; -import org.zanata.rest.service.URIHelper; import org.zanata.rest.editor.service.resource.StatisticResource; import com.google.common.collect.Lists; @@ -56,7 +56,7 @@ public Response getDocumentStatistics( @PathParam("versionSlug") String versionSlug, @PathParam("docId") String docId, @PathParam("localeId") String localeId) { - docId = URIHelper.convertFromDocumentURIId(docId); + docId = RestUtil.convertFromDocumentURIId(docId); HDocument doc = documentDAO.getByProjectIterationAndDocId(projectSlug, versionSlug, docId); if (doc == null) { diff --git a/server/zanata-war/src/main/java/org/zanata/rest/editor/service/TransMemoryMergeManager.java b/server/zanata-war/src/main/java/org/zanata/rest/editor/service/TransMemoryMergeManager.java index 21973c610e..3e386f4584 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/editor/service/TransMemoryMergeManager.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/editor/service/TransMemoryMergeManager.java @@ -20,29 +20,36 @@ */ package org.zanata.rest.editor.service; +import static org.zanata.async.AsyncTaskKey.joinFields; + import java.io.Serializable; import java.util.Objects; +import javax.annotation.Nullable; import javax.enterprise.context.Dependent; import javax.inject.Inject; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zanata.async.AsyncTaskHandle; import org.zanata.async.AsyncTaskHandleManager; +import org.zanata.async.AsyncTaskKey; +import org.zanata.async.GenericAsyncTaskKey; +import org.zanata.async.handle.MergeTranslationsTaskHandle; import org.zanata.async.handle.TransMemoryMergeTaskHandle; import org.zanata.common.LocaleId; -import org.zanata.model.HAccount; -import org.zanata.security.annotations.Authenticated; +import org.zanata.rest.dto.VersionTMMerge; +import org.zanata.security.ZanataIdentity; import org.zanata.service.TransMemoryMergeService; import org.zanata.webtrans.shared.model.DocumentId; -import org.zanata.webtrans.shared.model.ProjectIterationId; import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeCancelRequest; import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeRequest; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * @author Patrick Huang * pahuang@redhat.com @@ -51,20 +58,23 @@ public class TransMemoryMergeManager implements Serializable { private static final Logger log = LoggerFactory.getLogger(TransMemoryMergeManager.class); + private static final long serialVersionUID = 1364316697376958035L; + private static final String KEY_NAME = "TMMergeForVerKey"; + private final AsyncTaskHandleManager asyncTaskHandleManager; private final TransMemoryMergeService transMemoryMergeService; - private final HAccount authenticated; + private final ZanataIdentity identity; @Inject public TransMemoryMergeManager( AsyncTaskHandleManager asyncTaskHandleManager, TransMemoryMergeService transMemoryMergeService, - @Authenticated HAccount authenticated) { + ZanataIdentity identity) { this.asyncTaskHandleManager = asyncTaskHandleManager; this.transMemoryMergeService = transMemoryMergeService; - this.authenticated = authenticated; + this.identity = identity; } /** @@ -77,15 +87,14 @@ public TransMemoryMergeManager( * if there is already a task running */ public boolean startTransMemoryMerge(TransMemoryMergeRequest request) { - TransMemoryTaskKey key = - new TransMemoryTaskKey(request.projectIterationId, + TMMergeForDocTaskKey key = + new TMMergeForDocTaskKey( request.documentId, request.localeId); AsyncTaskHandle handleByKey = asyncTaskHandleManager.getHandleByKey(key); - if (handleByKey == null || handleByKey.isCancelled() - || handleByKey.isDone()) { + if (taskIsNotRunning(handleByKey)) { TransMemoryMergeTaskHandle handle = new TransMemoryMergeTaskHandle(); - handle.setTriggeredBy(authenticated.getUsername()); + handle.setTriggeredBy(identity.getAccountUsername()); asyncTaskHandleManager.registerTaskHandle(handle, key); transMemoryMergeService.executeMergeAsync(request, handle); return true; @@ -93,65 +102,83 @@ public boolean startTransMemoryMerge(TransMemoryMergeRequest request) { return false; } + @VisibleForTesting + protected static boolean taskIsNotRunning(@Nullable AsyncTaskHandle handleByKey) { + return handleByKey == null || handleByKey.isCancelled() + || handleByKey.isDone(); + } + public boolean cancelTransMemoryMerge(TransMemoryMergeCancelRequest request) { - TransMemoryTaskKey key = - new TransMemoryTaskKey(request.projectIterationId, + TMMergeForDocTaskKey key = + new TMMergeForDocTaskKey( request.documentId, request.localeId); AsyncTaskHandle handleByKey = asyncTaskHandleManager.getHandleByKey(key); - if (handleByKey != null && !(handleByKey.isDone() || handleByKey.isCancelled())) { - TransMemoryMergeTaskHandle handle = - (TransMemoryMergeTaskHandle) handleByKey; - String triggeredBy = handle.getTriggeredBy(); - if (Objects.equals(authenticated.getUsername(), triggeredBy)) { - handle.cancel(true); - handle.setCancelledTime(System.currentTimeMillis()); - handle.setCancelledBy(authenticated.getUsername()); - log.info("task: {} cancelled by its creator", handle); - return true; - } else { - log.warn("{} is attempting to cancel {}", authenticated.getUsername(), handle); - } + if (taskIsNotRunning(handleByKey)) { + return false; + } + TransMemoryMergeTaskHandle handle = + (TransMemoryMergeTaskHandle) handleByKey; + String triggeredBy = handle.getTriggeredBy(); + if (Objects.equals(identity.getAccountUsername(), triggeredBy)) { + handle.cancel(true); + handle.setCancelledTime(System.currentTimeMillis()); + handle.setCancelledBy(identity.getAccountUsername()); + log.info("task: {} cancelled by its creator", handle); + return true; + } else { + log.warn("{} is attempting to cancel {}", identity.getAccountUsername(), handle); } return false; } - static class TransMemoryTaskKey implements Serializable { + public AsyncTaskHandle start(Long versionId, VersionTMMerge mergeRequest) { + AsyncTaskKey key = makeKey(versionId, mergeRequest.getLocaleId()); + MergeTranslationsTaskHandle handleByKey = + (MergeTranslationsTaskHandle) asyncTaskHandleManager.getHandleByKey(key); + if (taskIsNotRunning(handleByKey)) { + handleByKey = new MergeTranslationsTaskHandle(key); + + handleByKey.setTriggeredBy(identity.getAccountUsername()); + asyncTaskHandleManager.registerTaskHandle(handleByKey, key); + transMemoryMergeService.startMergeTranslations(versionId, + mergeRequest, handleByKey); + } else { + log.warn( + "there is already a task running for version id {} and locale {}", + versionId, mergeRequest.getLocaleId()); + throw new UnsupportedOperationException("there is already a task running for version and locale"); + } + return handleByKey; + } - @SuppressFBWarnings("SE_BAD_FIELD") - private final ProjectIterationId projectIterationId; + @SuppressFBWarnings(value = "EQ_DOESNT_OVERRIDE_EQUALS", justification = "super class equals method is sufficient") + static class TMMergeForDocTaskKey extends + GenericAsyncTaskKey { + + private static final long serialVersionUID = -7210004008208642L; + private static final String KEY_NAME = "TMMergeForDocKey"; private final DocumentId documentId; private final LocaleId localeId; - TransMemoryTaskKey(ProjectIterationId projectIterationId, - DocumentId documentId, LocaleId localeId) { - - this.projectIterationId = projectIterationId; + TMMergeForDocTaskKey(DocumentId documentId, LocaleId localeId) { + // here we use numeric id to form the string id because it doesn't require URL encoding + super(joinFields(KEY_NAME, documentId.getId(), localeId)); this.documentId = documentId; this.localeId = localeId; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TransMemoryTaskKey that = (TransMemoryTaskKey) o; - return Objects.equals(projectIterationId, that.projectIterationId) - && Objects.equals(documentId, that.documentId) - && Objects.equals(localeId, that.localeId); - } - - @Override - public int hashCode() { - return Objects.hash(projectIterationId, documentId, localeId); - } - @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("projectIterationId", projectIterationId) - .add("documentId", documentId).add("localeId", localeId) + .add("documentId", documentId) + .add("localeId", localeId) .toString(); } } + + @VisibleForTesting + protected static AsyncTaskKey makeKey(Long versionId, LocaleId localeId) { + return new GenericAsyncTaskKey(joinFields(KEY_NAME, versionId, localeId)); + } } diff --git a/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectSearchResult.java b/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectSearchResult.java index baacf9315f..22e9b70b29 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectSearchResult.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectSearchResult.java @@ -20,6 +20,8 @@ */ package org.zanata.rest.search.dto; +import java.util.List; + import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.zanata.common.EntityStatus; @@ -35,6 +37,7 @@ public class ProjectSearchResult extends SearchResult { private String title; private long contributorCount; private EntityStatus status; + private List versions; public ProjectSearchResult() { this.setType(SearchResultType.Project); @@ -63,4 +66,12 @@ public EntityStatus getStatus() { public void setStatus(final EntityStatus status) { this.status = status; } + + public List getVersions() { + return versions; + } + + public void setVersions(List versions) { + this.versions = versions; + } } diff --git a/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectVersionSearchResult.java b/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectVersionSearchResult.java new file mode 100644 index 0000000000..2d3a3c609a --- /dev/null +++ b/server/zanata-war/src/main/java/org/zanata/rest/search/dto/ProjectVersionSearchResult.java @@ -0,0 +1,30 @@ +package org.zanata.rest.search.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.zanata.common.EntityStatus; +import org.zanata.rest.dto.SearchResult; + +/** + * @author Patrick Huang pahuang@redhat.com + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +public class ProjectVersionSearchResult extends SearchResult { + private static final long serialVersionUID = 1L; + private EntityStatus status; + + public ProjectVersionSearchResult() { + super.setType(SearchResultType.ProjectVersion); + } + + public ProjectVersionSearchResult(String slug, EntityStatus status) { + super(); + setId(slug); + this.status = status; + } + + public EntityStatus getStatus() { + return status; + } +} diff --git a/server/zanata-war/src/main/java/org/zanata/rest/search/service/SearchService.java b/server/zanata-war/src/main/java/org/zanata/rest/search/service/SearchService.java index 2c56c472ae..01f9652164 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/search/service/SearchService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/search/service/SearchService.java @@ -21,6 +21,21 @@ package org.zanata.rest.search.service; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + import org.apache.commons.lang.StringUtils; import org.apache.deltaspike.jpa.api.transaction.Transactional; import org.apache.lucene.queryparser.classic.ParseException; @@ -28,32 +43,26 @@ import org.zanata.dao.LocaleDAO; import org.zanata.dao.PersonDAO; import org.zanata.dao.ProjectDAO; +import org.zanata.dao.ProjectIterationDAO; import org.zanata.dao.VersionGroupDAO; import org.zanata.model.HAccount; import org.zanata.model.HIterationGroup; import org.zanata.model.HProject; +import org.zanata.model.HProjectIteration; import org.zanata.rest.dto.SearchResult; import org.zanata.rest.search.dto.GroupSearchResult; import org.zanata.rest.search.dto.PersonSearchResult; import org.zanata.rest.search.dto.ProjectSearchResult; +import org.zanata.rest.search.dto.ProjectVersionSearchResult; import org.zanata.rest.search.dto.SearchResults; +import org.zanata.rest.service.RestResource; import org.zanata.security.ZanataIdentity; import org.zanata.security.annotations.Authenticated; import org.zanata.service.GravatarService; import org.zanata.service.LocaleService; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; -import java.util.List; -import java.util.stream.Collectors; - -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; /** * @author Carlos Munoz camunoz@redhat.com @@ -62,11 +71,14 @@ @Path("/search") @Produces(APPLICATION_JSON) @Transactional(readOnly = true) -public class SearchService { +public class SearchService implements RestResource { @Inject private ProjectDAO projectDAO; + @Inject + private ProjectIterationDAO projectIterationDAO; + @Inject private PersonDAO personDAO; @@ -92,7 +104,8 @@ public class SearchService { public Response searchProjects( @QueryParam("q") @DefaultValue("") String query, @DefaultValue("1") @QueryParam("page") int page, - @DefaultValue("20") @QueryParam("sizePerPage") int sizePerPage) { + @DefaultValue("20") @QueryParam("sizePerPage") int sizePerPage, + @DefaultValue("false") @QueryParam("includeVersion") boolean includeVersion) { int offset = (validatePage(page) - 1) * validatePageSize(sizePerPage); @@ -110,12 +123,36 @@ public Response searchProjects( validatePageSize(sizePerPage), offset, false); } + + Map> projectSlugToVersions = + Maps.newHashMap(); + if (includeVersion && !projects.isEmpty()) { + List versions = projectIterationDAO + .searchByProjectsExcludeObsolete(projects); + versions.forEach(ver -> { + String projectSlug = ver.getProject().getSlug(); + List iterations = projectSlugToVersions + .getOrDefault(projectSlug, + Lists.newLinkedList()); + iterations.add(ver); + projectSlugToVersions.put(projectSlug, iterations); + }); + } List results = projects.stream().map(p -> { ProjectSearchResult result = new ProjectSearchResult(); result.setId(p.getSlug()); result.setStatus(p.getStatus()); result.setTitle(p.getName()); result.setDescription(p.getDescription()); + if (includeVersion) { + List iterations = + projectSlugToVersions.get(p.getSlug()); + result.setVersions(iterations == null ? null : iterations + .stream() + .map(iteration -> new ProjectVersionSearchResult( + iteration.getSlug(), iteration.getStatus())) + .collect(Collectors.toList())); + } // TODO: include contributor count when data is available return result; }).collect(Collectors.toList()); diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/AsyncProcessService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/AsyncProcessService.java new file mode 100644 index 0000000000..0bfc24c5e8 --- /dev/null +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/AsyncProcessService.java @@ -0,0 +1,248 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.zanata.rest.service; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.jetbrains.annotations.NotNull; +import org.zanata.async.AsyncTaskHandle; +import org.zanata.async.AsyncTaskHandleManager; +import org.zanata.async.UserTriggeredTaskHandle; +import org.zanata.exception.AuthorizationException; +import org.zanata.rest.dto.ProcessStatus; +import org.zanata.rest.editor.service.SuggestionsService; +import org.zanata.security.ZanataIdentity; +import org.zanata.security.annotations.CheckRole; +import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeCancelRequest; +import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeRequest; + +import com.google.common.annotations.VisibleForTesting; +import com.webcohesion.enunciate.metadata.rs.TypeHint; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * This endpoint should let us control and query all async tasks. + * + * TODO AsynchronousProcessResourceService are specific for CLI push and pull + * TODO org.zanata.rest.editor.service.SuggestionsService has specific methods for TM merge + * + * @see AsynchronousProcessResourceService + * @see SuggestionsService#merge(TransMemoryMergeRequest) + * @see SuggestionsService#cancelMerge(TransMemoryMergeCancelRequest) + * + * @author Patrick Huang + * pahuang@redhat.com + */ +@Path("/process") +@Produces(MediaType.APPLICATION_JSON) +public class AsyncProcessService implements RestResource { + private static final long serialVersionUID = 1L; + + @Inject + private AsyncTaskHandleManager asyncTaskHandleManager; + + @Inject + private ZanataIdentity identity; + + @SuppressFBWarnings("SE_BAD_FIELD") + @Context + private UriInfo uriInfo; + + public AsyncProcessService() { + } + + // UriInfo can not be injected via constructor injection + @VisibleForTesting + AsyncProcessService(AsyncTaskHandleManager taskHandleManager, + ZanataIdentity identity, UriInfo uriInfo) { + asyncTaskHandleManager = taskHandleManager; + this.identity = identity; + this.uriInfo = uriInfo; + } + + + /** + * Get an async task's status if user has read permission to the task. + * + * @param keyId + * task id + * @return The following response status codes will be returned from this + * operation:
    + * OK(200) - The contents of the response will indicate the process + * identifier which may be used to query for its status or a message + * indicating what happened.
    + * NOT FOUND(404) - if the task can not be found or user has no + * permission to view its status.
    + * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @Path("key/{keyId}") + @GET + @TypeHint(ProcessStatus.class) + public Response getAsyncProcessStatus(@PathParam("keyId") String keyId) { + AsyncTaskHandle handle = asyncTaskHandleManager.getHandleByKeyId(keyId); + if (handle == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + if (!handle.isVisibleTo(identity)) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + ProcessStatus status = handleToProcessStatus(handle, + uriInfo.getRequestUri().toString()); + return Response.ok(status).build(); + } + + /** + * Get statuses for all async tasks. This is for admin only. + * + * @param includeFinished + * whether to include finished tasks + * + * @return The following response status codes will be returned from this + * operation:
    + * OK(200) - The contents of the response will indicate all + * background processes which may be used to query for its status or + * a message indicating what happened.
    + * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @GET + @CheckRole("admin") + public Response getAllAsyncProcessStatuses( + @QueryParam("includeFinished") @DefaultValue("false") boolean includeFinished) { + Map> tasks; + if (includeFinished) { + tasks = asyncTaskHandleManager.getAllTasks(); + } else { + tasks = asyncTaskHandleManager.getRunningTasks(); + } + List processStatuses = tasks.entrySet().stream() + .map(taskEntry -> handleToProcessStatus(taskEntry.getValue(), + uriInfo.getBaseUri() + "process/key/" + + taskEntry.getKey())) + .collect(Collectors.toList()); + return Response.ok(processStatuses).build(); + } + + /** + * Cancel a specific async task. + * + * @param keyId + * task id + * + * @return The following response status codes will be returned from this + * operation:
    + * OK(200) - The contents of the response will indicate the process + * identifier which may be used to query for its status or a message + * indicating what happened.
    + * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @POST + @Path("cancel/key/{keyId}") + public Response cancelAsyncProcess(@PathParam("keyId") String keyId) { + AsyncTaskHandle handle = asyncTaskHandleManager.getHandleByKeyId(keyId); + if (handle == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + if (!handle.isRunning()) { + ProcessStatus entity = handleToProcessStatus(handle, + uriInfo.getBaseUri() + "process/key/" + keyId); + return Response.ok(entity).build(); + } + + if (!handle.canCancel(identity)) { + String message = handle instanceof UserTriggeredTaskHandle + ? "Only the task owner or admin can cancel the task:" + + keyId + : "Only admin can cancel the task:" + + keyId; + throw new AuthorizationException(message); + } + + handle.cancel(true); + handle.setCancelledBy(identity.getAccountUsername()); + handle.setCancelledTime(System.currentTimeMillis()); + + ProcessStatus processStatus = handleToProcessStatus(handle, + uriInfo.getBaseUri() + "process/cancel/key/" + keyId); + return Response.ok(processStatus).build(); + } + + @NotNull + public static ProcessStatus handleToProcessStatus(AsyncTaskHandle handle, + String url) { + ProcessStatus status = new ProcessStatus(); + status.setStatusCode( + handle.isDone() ? ProcessStatus.ProcessStatusCode.Finished + : ProcessStatus.ProcessStatusCode.Running); + int percentComplete = 100; + if (handle.getMaxProgress() > 0) { + percentComplete = (int) (handle.getCurrentProgress() * 100 + / handle.getMaxProgress()); + } + status.setPercentageComplete(percentComplete); + status.setUrl(url); + if (handle.isCancelled()) { + status.setStatusCode(ProcessStatus.ProcessStatusCode.Cancelled); + status.addMessage("Cancelled by " + handle.getCancelledBy()); + } else if (handle.isDone()) { + Object result = null; + try { + result = handle.getResult(); + } catch (InterruptedException e) { + // The process was forcefully cancelled + status.setStatusCode(ProcessStatus.ProcessStatusCode.Cancelled); + status.addMessage(e.getMessage()); + } catch (ExecutionException e) { + // Exception thrown while running the task + status.setStatusCode(ProcessStatus.ProcessStatusCode.Failed); + status.addMessage(e.getCause().getMessage()); + } catch (Exception e) { + status.setStatusCode(ProcessStatus.ProcessStatusCode.Failed); + status.addMessage("Unknown exception:" + e.getMessage()); + } + // TODO Need to find a generic way of returning all object types. + if (result != null) { + status.addMessage(result.toString()); + } + } + return status; + } +} diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/AsynchronousProcessResourceService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/AsynchronousProcessResourceService.java index b3aa31f672..2b039bff87 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/AsynchronousProcessResourceService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/AsynchronousProcessResourceService.java @@ -20,13 +20,12 @@ */ package org.zanata.rest.service; -import java.io.Serializable; -import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.inject.Named; + +import org.apache.commons.lang3.StringUtils; import org.apache.deltaspike.jpa.api.transaction.Transactional; import org.zanata.async.AsyncTaskHandle; import org.zanata.async.AsyncTaskHandleManager; @@ -41,6 +40,7 @@ import org.zanata.model.type.TranslationSourceType; import org.zanata.rest.NoSuchEntityException; import org.zanata.rest.ReadOnlyEntityException; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.ProcessStatus; import org.zanata.rest.dto.resource.Resource; import org.zanata.rest.dto.resource.TranslationsResource; @@ -48,10 +48,11 @@ import org.zanata.service.DocumentService; import org.zanata.service.LocaleService; import org.zanata.service.TranslationService; -import com.google.common.collect.Lists; +import javax.ws.rs.BadRequestException; import javax.ws.rs.NotFoundException; import javax.ws.rs.Path; + import static org.zanata.rest.dto.ProcessStatus.ProcessStatusCode; /** @@ -69,6 +70,7 @@ public class AsynchronousProcessResourceService implements AsynchronousProcessResource { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory .getLogger(AsynchronousProcessResourceService.class); + private static final long serialVersionUID = -5915271018788588841L; @Inject private LocaleService localeServiceImpl; @@ -83,8 +85,6 @@ public class AsynchronousProcessResourceService @Inject private ProjectIterationDAO projectIterationDAO; @Inject - private ResourceUtils resourceUtils; - @Inject private ZanataIdentity identity; @Override @@ -96,7 +96,7 @@ public ProcessStatus startSourceDocCreation(final String idNoSlash, retrieveAndCheckIteration(projectSlug, iterationSlug, true); // Check permission identity.checkPermission(hProjectIteration, "import-template"); - resourceUtils.validateExtensions(extensions); // gettext, comment + ResourceUtils.validateExtensions(extensions); // gettext, comment HDocument document = documentDAO .getByDocIdAndIteration(hProjectIteration, resource.getName()); // already existing non-obsolete document. @@ -112,13 +112,13 @@ public ProcessStatus startSourceDocCreation(final String idNoSlash, } String name = "SourceDocCreation: " + projectSlug + "-" + iterationSlug + "-" + idNoSlash; - AsyncTaskHandle handle = new AsyncTaskHandle(); - Serializable taskId = asyncTaskHandleManager.registerTaskHandle(handle); + AsyncTaskHandle handle = new AsyncTaskHandle<>(); + String keyId = asyncTaskHandleManager.registerTaskHandle(handle); documentServiceImpl .saveDocumentAsync(projectSlug, iterationSlug, resource, extensions, copytrans, true, handle); - logWhenUploadComplete(handle, name, taskId); - return getProcessStatus(taskId.toString()); // TODO Change to return 202 + logWhenUploadComplete(handle, name, keyId); + return getProcessStatus(keyId); // TODO Change to return 202 // Accepted, // with a url to get the // progress @@ -129,28 +129,46 @@ public ProcessStatus startSourceDocCreationOrUpdate(final String idNoSlash, final String projectSlug, final String iterationSlug, final Resource resource, final Set extensions, final boolean copytrans) { + String docId = RestUtil.convertFromDocumentURIId(idNoSlash); + return startSourceDocCreationOrUpdateProcess(projectSlug, + iterationSlug, resource, extensions, docId, copytrans); + } + + @Override + public ProcessStatus startSourceDocCreationOrUpdateWithDocId( + String projectSlug, String iterationSlug, Resource resource, + Set extensions, String docId) { + boolean copyTrans = false; + return startSourceDocCreationOrUpdateProcess(projectSlug, + iterationSlug, resource, extensions, docId, copyTrans); + } + + private ProcessStatus startSourceDocCreationOrUpdateProcess( + String projectSlug, String iterationSlug, Resource resource, + Set extensions, String docId, boolean copyTrans) { + if (StringUtils.isBlank(docId)) { + throw new BadRequestException("missing id"); + } HProjectIteration hProjectIteration = retrieveAndCheckIteration(projectSlug, iterationSlug, true); - resourceUtils.validateExtensions(extensions); // gettext, comment + ResourceUtils.validateExtensions(extensions); // gettext, comment // Check permission identity.checkPermission(hProjectIteration, "import-template"); String name = "SourceDocCreationOrUpdate: " + projectSlug + "-" - + iterationSlug + "-" + idNoSlash; + + iterationSlug + "-" + docId; AsyncTaskHandle handle = new AsyncTaskHandle(); - Serializable taskId = asyncTaskHandleManager.registerTaskHandle(handle); + String keyId = asyncTaskHandleManager.registerTaskHandle(handle); documentServiceImpl .saveDocumentAsync(projectSlug, iterationSlug, resource, - extensions, copytrans, true, handle); - logWhenUploadComplete(handle, name, taskId); - return getProcessStatus(taskId.toString()); // TODO Change to return 202 - // Accepted, - // with a url to get the - // progress + extensions, copyTrans, true, handle); + logWhenUploadComplete(handle, name, keyId); + return getProcessStatus(keyId); // TODO Change to return 202 + // Accepted, with a url to get the progress } private void logWhenUploadComplete( AsyncTaskHandle taskHandle, - final String taskName, final Serializable taskId) { + final String taskName, final String taskId) { taskHandle.whenTaskComplete((result, throwable) -> { if (throwable != null) { log.warn("async upload failed. id={}, job={}", taskId, taskName, @@ -180,12 +198,27 @@ public ProcessStatus startTranslatedDocCreationOrUpdate( final TranslationsResource translatedDoc, final Set extensions, final String merge, final boolean assignCreditToUploader) { + final String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return startTranslatedDocCreationOrUpdateWithDocId(projectSlug, + iterationSlug, locale, translatedDoc, id, extensions, merge, + assignCreditToUploader); + } + + @Override + public ProcessStatus startTranslatedDocCreationOrUpdateWithDocId( + String projectSlug, String iterationSlug, LocaleId locale, + TranslationsResource translatedDoc, String docId, + Set extensions, + String merge, boolean assignCreditToUploader) { // check security (cannot be on @Restrict as it refers to method // parameters) identity.checkPermission("modify-translation", this.localeServiceImpl.getByLocaleId(locale), this.getSecuredIteration(projectSlug, iterationSlug) .getProject()); + if (StringUtils.isBlank(docId)) { + throw new BadRequestException("missing docId"); + } MergeType mergeType; try { mergeType = MergeType.valueOf(merge.toUpperCase()); @@ -195,59 +228,29 @@ public ProcessStatus startTranslatedDocCreationOrUpdate( status.getMessages().add("bad merge type " + merge); return status; } - final String id = URIHelper.convertFromDocumentURIId(idNoSlash); final MergeType finalMergeType = mergeType; - String taskName = "TranslatedDocUpload: "+projectSlug+"-"+iterationSlug+"-"+idNoSlash; - AsyncTaskHandle handle = new AsyncTaskHandle(); - Serializable taskId = asyncTaskHandleManager.registerTaskHandle(handle); + String taskName = "TranslatedDocUpload: "+projectSlug+"-"+iterationSlug+"-"+ + docId; + AsyncTaskHandle handle = new AsyncTaskHandle<>(); + String keyId = asyncTaskHandleManager.registerTaskHandle(handle); translationServiceImpl.translateAllInDocAsync(projectSlug, - iterationSlug, id, locale, translatedDoc, extensions, - finalMergeType, assignCreditToUploader, true, handle, - TranslationSourceType.API_UPLOAD); - logWhenUploadComplete(handle, taskName, taskId); - return this.getProcessStatus(taskId.toString()); + iterationSlug, docId, locale, translatedDoc, extensions, + finalMergeType, assignCreditToUploader, true, handle, + TranslationSourceType.API_UPLOAD); + logWhenUploadComplete(handle, taskName, keyId); + return this.getProcessStatus(keyId); } @Override + @SuppressWarnings("rawtypes") public ProcessStatus getProcessStatus(String processId) { AsyncTaskHandle handle = - asyncTaskHandleManager.getHandleByKey(processId); + asyncTaskHandleManager.getHandleByKeyId(processId); if (handle == null) { throw new NotFoundException( "A process was not found for id " + processId); } - ProcessStatus status = new ProcessStatus(); - status.setStatusCode(handle.isDone() ? ProcessStatusCode.Finished - : ProcessStatusCode.Running); - int perComplete = 100; - if (handle.getMaxProgress() > 0) { - perComplete = (handle.getCurrentProgress() * 100 - / handle.getMaxProgress()); - } - status.setPercentageComplete(perComplete); - status.setUrl("" + processId); - if (handle.isDone()) { - Object result = null; - try { - result = handle.getResult(); - } catch (InterruptedException e) { - // The process was forcefully cancelled - status.setStatusCode(ProcessStatusCode.Failed); - status.setMessages(Lists.newArrayList(e.getMessage())); - } catch (ExecutionException e) { - // Exception thrown while running the task - status.setStatusCode(ProcessStatusCode.Failed); - status.setMessages( - Lists.newArrayList(e.getCause().getMessage())); - } - // TODO Need to find a generic way of returning all object types. - // Since the only current - // scenario involves lists of strings, hardcoding to that - if (result != null && result instanceof List) { - status.getMessages().addAll((List) result); - } - } - return status; + return AsyncProcessService.handleToProcessStatus(handle, processId); } private HProjectIteration retrieveAndCheckIteration(String projectSlug, diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/ProjectVersionService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/ProjectVersionService.java index 493440991f..89889ba541 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/ProjectVersionService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/ProjectVersionService.java @@ -4,26 +4,34 @@ import static org.zanata.common.EntityStatus.OBSOLETE; import static org.zanata.common.EntityStatus.READONLY; import static org.zanata.webtrans.server.rpc.GetTransUnitsNavigationService.TextFlowResultTransformer; + import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; + import javax.inject.Inject; import javax.inject.Named; import javax.ws.rs.DefaultValue; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.EntityTag; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import com.google.common.base.Objects; + import org.apache.commons.lang.StringUtils; import org.apache.deltaspike.jpa.api.transaction.Transactional; import org.zanata.ApplicationConfiguration; +import org.zanata.async.AsyncTaskHandle; +import org.zanata.async.handle.MergeTranslationsTaskHandle; import org.zanata.common.ContentState; import org.zanata.common.EntityStatus; +import org.zanata.common.LocaleId; import org.zanata.common.ProjectType; import org.zanata.dao.DocumentDAO; import org.zanata.dao.ProjectDAO; @@ -37,18 +45,24 @@ import org.zanata.model.HTextFlow; import org.zanata.rest.NoSuchEntityException; import org.zanata.rest.ReadOnlyEntityException; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.LocaleDetails; +import org.zanata.rest.dto.ProcessStatus; import org.zanata.rest.dto.ProjectIteration; import org.zanata.rest.dto.TransUnitStatus; import org.zanata.rest.dto.User; +import org.zanata.rest.dto.VersionTMMerge; import org.zanata.rest.dto.resource.ResourceMeta; +import org.zanata.rest.editor.service.TransMemoryMergeManager; import org.zanata.rest.editor.service.UserService; -import org.zanata.webtrans.shared.search.FilterConstraints; import org.zanata.security.ZanataIdentity; import org.zanata.service.ConfigurationService; import org.zanata.service.LocaleService; import org.zanata.webtrans.shared.model.DocumentId; +import org.zanata.webtrans.shared.search.FilterConstraints; + import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; import com.google.common.collect.Lists; /** @@ -86,6 +100,8 @@ public class ProjectVersionService implements ProjectVersionResource { private ApplicationConfiguration applicationConfiguration; @Context private UriInfo uri; + @Inject + private TransMemoryMergeManager transMemoryMergeManager; @Override public Response head(@PathParam("projectSlug") String projectSlug, @@ -112,21 +128,13 @@ public Response put(@PathParam("projectSlug") String projectSlug, return projTypeError; } HProject hProject = projectDAO.getBySlug(projectSlug); - if (hProject == null) { - return Response.status(Response.Status.NOT_FOUND) - .entity("Project \'" + projectSlug + "\' not found.") - .build(); - } else if (Objects.equal(hProject.getStatus(), OBSOLETE)) { - // Project is Obsolete - return Response.status(Response.Status.NOT_FOUND) - .entity("Project \'" + projectSlug + "\' not found.") - .build(); - } else if (Objects.equal(hProject.getStatus(), READONLY)) { - // Project is ReadOnly - return Response.status(Response.Status.FORBIDDEN) - .entity("Project \'" + projectSlug + "\' is read-only.") - .build(); + + Optional projectResponse = + getResponseIfProjectIsNotActive(hProject, projectSlug); + if (projectResponse.isPresent()) { + return projectResponse.get(); } + HProjectIteration hProjectVersion = projectIterationDAO.getBySlug(projectSlug, versionSlug); if (hProjectVersion == null) { @@ -179,6 +187,26 @@ public Response put(@PathParam("projectSlug") String projectSlug, return response.tag(etag).build(); } + private Optional getResponseIfProjectIsNotActive( + HProject hProject, String projectSlug) { + if (hProject == null) { + return Optional.of(Response.status(Response.Status.NOT_FOUND) + .entity("Project \'" + projectSlug + "\' not found.") + .build()); + } else if (Objects.equal(hProject.getStatus(), OBSOLETE)) { + // Project is Obsolete + return Optional.of(Response.status(Response.Status.NOT_FOUND) + .entity("Project \'" + projectSlug + "\' not found.") + .build()); + } else if (Objects.equal(hProject.getStatus(), READONLY)) { + // Project is ReadOnly + return Optional.of(Response.status(Response.Status.FORBIDDEN) + .entity("Project \'" + projectSlug + "\' is read-only.") + .build()); + } + return Optional.empty(); + } + @Override public Response sampleConfiguration( @PathParam("projectSlug") String projectSlug, @@ -279,7 +307,7 @@ public Response getTransUnitStatus( if (StringUtils.isEmpty(noSlashDocId)) { return Response.status(Response.Status.NOT_FOUND).build(); } - String docId = URIHelper.convertFromDocumentURIId(noSlashDocId); + String docId = RestUtil.convertFromDocumentURIId(noSlashDocId); HDocument document = documentDAO .getByProjectIterationAndDocId(projectSlug, versionSlug, docId); if (document == null) { @@ -308,6 +336,82 @@ public Response getTransUnitStatus( return Response.ok(entity).build(); } + /** + * + * Trigger a TM merge for the target version. It will run in the background + * and pre-fill translations from TM based on user selected criteria. + * + * @param projectSlug + * The project slug/ID + * @param versionSlug + * The project version slug/ID + * @param mergeRequest + * TM merge criteria + * @return The following response status codes will be returned from this + * operation:
    + * ACCEPTED(202) - If the process is successfully triggered.
    + * UNACCEPTED(400) - If the incoming payload is invalid.
    + * NOT FOUND(404) - If no project or version was found for the given + * project slug and version slug.
    + * FORBIDDEN(403) - If the user was not allowed to create/modify the + * project iteration. In this case an error message is contained in + * the response.
    + * UNAUTHORIZED(401) - If the user does not have the proper + * permissions to perform this operation.
    + * INTERNAL SERVER ERROR(500) - If there is an unexpected error in + * the server while performing this operation. + */ + @POST + @Path(VERSION_SLUG_TEMPLATE + "/tm-merge") + public Response prefillWithTM(@PathParam("projectSlug") String projectSlug, + @PathParam("versionSlug") String versionSlug, + VersionTMMerge mergeRequest) { + int percent = mergeRequest.getThresholdPercent(); + if (percent < 80 || percent > 100) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\":\"percentThreshold must be between 80 and 100\"}") + .build(); + } + HProject hProject = projectDAO.getBySlug(projectSlug); + + Optional projectResponse = + getResponseIfProjectIsNotActive(hProject, projectSlug); + if (projectResponse.isPresent()) { + return projectResponse.get(); + } + HProjectIteration version = + projectIterationDAO.getBySlug(hProject, versionSlug); + if (version == null || version.getStatus() == OBSOLETE) { + return Response.status(Response.Status.NOT_FOUND) + .entity("Project version \'" + projectSlug + ":" + + versionSlug + "\' not found.") + .build(); + } else if (version.getStatus() == READONLY) { + return Response.status(Response.Status.FORBIDDEN) + .entity("Project version \'" + projectSlug + ":" + + versionSlug + "\' is readonly.") + .build(); + } else if (version.getDocuments().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\":\"project version has no documents\"}") + .build(); + } + LocaleId localeId = mergeRequest.getLocaleId(); + HLocale hLocale = localeServiceImpl.getByLocaleId(localeId); + if (hLocale == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + identity.checkPermission("modify-translation", hProject, hLocale); + AsyncTaskHandle handle = transMemoryMergeManager + .start(version.getId(), mergeRequest); + + ProcessStatus processStatus = AsyncProcessService + .handleToProcessStatus(handle, + uri.getBaseUri() + "process/key/" + handle.getKeyId()); + return Response.accepted(processStatus).build(); + } + @VisibleForTesting protected HProjectIteration retrieveAndCheckIteration(String projectSlug, String versionSlug, boolean writeOperation) { diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/SourceDocResourceService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/SourceDocResourceService.java index f4f878fc00..7c1016679a 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/SourceDocResourceService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/SourceDocResourceService.java @@ -35,9 +35,13 @@ import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.inject.Inject; import javax.inject.Named; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.lang3.StringUtils; import org.apache.deltaspike.jpa.api.transaction.Transactional; import org.zanata.common.EntityStatus; import org.zanata.common.LocaleId; @@ -51,6 +55,7 @@ import org.zanata.model.HTextFlow; import org.zanata.rest.NoSuchEntityException; import org.zanata.rest.ReadOnlyEntityException; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.resource.Resource; import org.zanata.rest.dto.resource.ResourceMeta; import org.zanata.rest.dto.resource.TextFlow; @@ -69,10 +74,13 @@ public class SourceDocResourceService implements SourceDocResource { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SourceDocResourceService.class); + private static final long serialVersionUID = 7787405987851272827L; @Context + @SuppressFBWarnings(value = "SE_BAD_FIELD") private Request request; @Context + @SuppressFBWarnings(value = "SE_BAD_FIELD") private UriInfo uri; /** @@ -141,7 +149,7 @@ public Response post(Resource resource, Set extensions, boolean copytrans) { identity.checkPermission(getSecuredIteration(), "import-template"); HProjectIteration hProjectIteration = retrieveAndCheckIteration(true); - resourceUtils.validateExtensions(extensions); // gettext, comment + ResourceUtils.validateExtensions(extensions); // gettext, comment String resourceName = resource.getName(); if (!Pattern.matches(SourceDocResource.RESOURCE_NAME_REGEX, resourceName)) { @@ -176,20 +184,30 @@ public Response post(Resource resource, Set extensions, @Override public Response getResource(String idNoSlash, Set extensions) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return getResourceWithDocId(id, extensions); + } + + @Override + public Response getResourceWithDocId(String docId, Set extensions) { log.debug("start get resource"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } HProjectIteration hProjectIteration = retrieveAndCheckIteration(false); - resourceUtils.validateExtensions(extensions); - final Set extSet = new HashSet(extensions); + ResourceUtils.validateExtensions(extensions); + final Set extSet = new HashSet<>(extensions); EntityTag etag = eTagUtils.generateETagForDocument(hProjectIteration, - id, extSet); + docId, extSet); Response.ResponseBuilder response = request.evaluatePreconditions(etag); if (response != null) { return response.build(); } HDocument doc = - documentDAO.getByDocIdAndIteration(hProjectIteration, id); + documentDAO.getByDocIdAndIteration(hProjectIteration, docId); if (doc == null || doc.isObsolete()) { + // TODO: return Problem DTO, https://tools.ietf.org/html/rfc7807 return Response.status(Response.Status.NOT_FOUND) .entity("document not found").build(); } @@ -215,20 +233,33 @@ public Response getResource(String idNoSlash, Set extensions) { @Override public Response putResource(String idNoSlash, Resource resource, Set extensions, boolean copytrans) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return putResourceWithDocId(resource, id, extensions, copytrans); + } + + @Override + public Response putResourceWithDocId(Resource resource, String docId, + Set extensions, boolean copytrans) { identity.checkPermission(getSecuredIteration(), "import-template"); log.debug("start put resource"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing docId").build(); + } Response.ResponseBuilder response; HProjectIteration hProjectIteration = retrieveAndCheckIteration(true); - resourceUtils.validateExtensions(extensions); + ResourceUtils.validateExtensions(extensions); HDocument document = - this.documentDAO.getByDocIdAndIteration(hProjectIteration, id); + this.documentDAO.getByDocIdAndIteration(hProjectIteration, + docId); if (document == null || document.isObsolete()) { - response = Response.created(uri.getAbsolutePath()); + response = Response.created( + UriBuilder.fromUri(uri.getAbsolutePath()) + .queryParam("docId", docId).build()); } else { response = Response.ok(); } - resource.setName(id); + resource.setName(docId); document = this.documentServiceImpl.saveDocument(projectSlug, iterationSlug, resource, extensions, copytrans); EntityTag etag = eTagUtils.generateETagForDocument(hProjectIteration, @@ -239,34 +270,53 @@ public Response putResource(String idNoSlash, Resource resource, @Override public Response deleteResource(String idNoSlash) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return deleteResourceWithDocId(id); + } + + @Override + public Response deleteResourceWithDocId(String docId) { identity.checkPermission(getSecuredIteration(), "import-template"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } HProjectIteration hProjectIteration = retrieveAndCheckIteration(true); EntityTag etag = eTagUtils.generateETagForDocument(hProjectIteration, - id, new HashSet()); + docId, new HashSet()); Response.ResponseBuilder response = request.evaluatePreconditions(etag); if (response != null) { return response.build(); } HDocument document = - documentDAO.getByDocIdAndIteration(hProjectIteration, id); + documentDAO.getByDocIdAndIteration(hProjectIteration, docId); documentServiceImpl.makeObsolete(document); return Response.ok().build(); } @Override public Response getResourceMeta(String idNoSlash, Set extensions) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return getResourceMetaWithDocId(id, extensions); + } + + @Override + public Response getResourceMetaWithDocId(String docId, + Set extensions) { log.debug("start to get resource meta"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } HProjectIteration hProjectIteration = retrieveAndCheckIteration(false); EntityTag etag = eTagUtils.generateETagForDocument(hProjectIteration, - id, extensions); + docId, extensions); Response.ResponseBuilder response = request.evaluatePreconditions(etag); if (response != null) { return response.build(); } HDocument doc = - documentDAO.getByDocIdAndIteration(hProjectIteration, id); + documentDAO.getByDocIdAndIteration(hProjectIteration, docId); if (doc == null) { return Response.status(Response.Status.NOT_FOUND) .entity("document not found").build(); @@ -283,12 +333,22 @@ public Response getResourceMeta(String idNoSlash, Set extensions) { @Override public Response putResourceMeta(String idNoSlash, ResourceMeta messageBody, Set extensions) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return putResourceMetaWithDocId(messageBody, id , extensions); + } + + @Override + public Response putResourceMetaWithDocId(ResourceMeta messageBody, + String docId, Set extensions) { identity.checkPermission(getSecuredIteration(), "import-template"); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } log.debug("start to put resource meta"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); HProjectIteration hProjectIteration = retrieveAndCheckIteration(true); EntityTag etag = eTagUtils.generateETagForDocument(hProjectIteration, - id, extensions); + docId, extensions); Response.ResponseBuilder response = request.evaluatePreconditions(etag); if (response != null) { return response.build(); @@ -296,7 +356,7 @@ public Response putResourceMeta(String idNoSlash, ResourceMeta messageBody, log.debug("pass evaluation"); log.debug("put resource meta: {}", messageBody); HDocument document = - documentDAO.getByDocIdAndIteration(hProjectIteration, id); + documentDAO.getByDocIdAndIteration(hProjectIteration, docId); if (document == null) { return Response.status(Response.Status.NOT_FOUND).build(); } @@ -310,7 +370,7 @@ public Response putResourceMeta(String idNoSlash, ResourceMeta messageBody, document.getRevision() + 1); if (changed) { documentDAO.flush(); - etag = eTagUtils.generateETagForDocument(hProjectIteration, id, + etag = eTagUtils.generateETagForDocument(hProjectIteration, docId, extensions); } log.debug("put resource meta successfully"); diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/TranslatedDocResourceService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/TranslatedDocResourceService.java index cc1a4299ab..844656a04f 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/TranslatedDocResourceService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/TranslatedDocResourceService.java @@ -20,7 +20,6 @@ */ package org.zanata.rest.service; -import java.util.HashSet; import java.util.List; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -40,12 +39,11 @@ import javax.inject.Named; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.lang3.StringUtils; import org.apache.deltaspike.jpa.api.transaction.Transactional; -import org.zanata.ApplicationConfiguration; import org.zanata.common.LocaleId; import org.zanata.common.MergeType; import org.zanata.dao.DocumentDAO; -import org.zanata.dao.ProjectDAO; import org.zanata.dao.ProjectIterationDAO; import org.zanata.dao.TextFlowTargetDAO; import org.zanata.model.HDocument; @@ -53,9 +51,9 @@ import org.zanata.model.HProjectIteration; import org.zanata.model.HTextFlowTarget; import org.zanata.model.type.TranslationSourceType; +import org.zanata.rest.RestUtil; import org.zanata.rest.dto.resource.TranslationsResource; import org.zanata.security.ZanataIdentity; -import org.zanata.service.CopyTransService; import org.zanata.service.LocaleService; import org.zanata.service.TranslationService; import com.google.common.base.Optional; @@ -71,6 +69,7 @@ public class TranslatedDocResourceService implements TranslatedDocResource { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory .getLogger(TranslatedDocResourceService.class); + private static final long serialVersionUID = -5855787114970845084L; // security actions // private static final String ACTION_IMPORT_TEMPLATE = "import-template"; @@ -96,20 +95,19 @@ public class TranslatedDocResourceService implements TranslatedDocResource { @Context private MediaType requestContentType; @Context + @SuppressFBWarnings("SE_BAD_FIELD") private HttpHeaders headers; @Context + @SuppressFBWarnings("SE_BAD_FIELD") private Request request; @Context + @SuppressFBWarnings("SE_BAD_FIELD") private UriInfo uri; @Inject private ZanataIdentity identity; @Inject - private ApplicationConfiguration applicationConfiguration; - @Inject private ProjectIterationDAO projectIterationDAO; @Inject - private ProjectDAO projectDAO; - @Inject private DocumentDAO documentDAO; @Inject private TextFlowTargetDAO textFlowTargetDAO; @@ -118,8 +116,6 @@ public class TranslatedDocResourceService implements TranslatedDocResource { @Inject private ETagUtils eTagUtils; @Inject - private CopyTransService copyTransServiceImpl; - @Inject private RestSlugValidator restSlugValidator; @Inject private TranslationService translationServiceImpl; @@ -129,8 +125,19 @@ public class TranslatedDocResourceService implements TranslatedDocResource { @Override public Response getTranslations(String idNoSlash, LocaleId locale, Set extensions, boolean skeletons, String eTag) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return getTranslationsWithDocId(locale, id, extensions, skeletons, eTag); + } + + @Override + public Response getTranslationsWithDocId(LocaleId locale, String docId, + Set extensions, boolean createSkeletons, String eTag) { log.debug("start to get translation"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); + if (StringUtils.isBlank(docId)) { + // TODO: return Problem DTO, https://tools.ietf.org/html/rfc7807 + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } HProjectIteration hProjectIteration = restSlugValidator .retrieveAndCheckIteration(projectSlug, iterationSlug, false); HLocale hLocale = restSlugValidator.validateTargetLocale(locale, @@ -138,7 +145,7 @@ public Response getTranslations(String idNoSlash, LocaleId locale, ResourceUtils.validateExtensions(extensions); // Check Etag header EntityTag generatedEtag = eTagUtils.generateETagForTranslatedDocument( - hProjectIteration, id, hLocale); + hProjectIteration, docId, hLocale); List requestedEtagHeaders = headers.getRequestHeader(HttpHeaders.IF_NONE_MATCH); if (requestedEtagHeaders != null && !requestedEtagHeaders.isEmpty()) { @@ -151,7 +158,7 @@ public Response getTranslations(String idNoSlash, LocaleId locale, return response.build(); } HDocument document = - documentDAO.getByDocIdAndIteration(hProjectIteration, id); + documentDAO.getByDocIdAndIteration(hProjectIteration, docId); if (document == null || document.isObsolete()) { return Response.status(Status.NOT_FOUND).build(); } @@ -162,7 +169,7 @@ public Response getTranslations(String idNoSlash, LocaleId locale, boolean foundData = resourceUtils.transferToTranslationsResource( translationResource, document, hLocale, extensions, hTargets, Optional. absent()); - if (!foundData && !skeletons) { + if (!foundData && !createSkeletons) { return Response.status(Status.NOT_FOUND).build(); } // TODO lastChanged @@ -172,21 +179,30 @@ public Response getTranslations(String idNoSlash, LocaleId locale, @Override public Response deleteTranslations(String idNoSlash, LocaleId locale) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return deleteTranslationsWithDocId(locale, id); + } + + @Override + public Response deleteTranslationsWithDocId(LocaleId locale, String docId) { identity.checkPermission(getSecuredIteration().getProject(), "modify-translation"); - String id = URIHelper.convertFromDocumentURIId(idNoSlash); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } HProjectIteration hProjectIteration = restSlugValidator .retrieveAndCheckIteration(projectSlug, iterationSlug, true); HLocale hLocale = restSlugValidator.validateTargetLocale(locale, projectSlug, iterationSlug); EntityTag etag = eTagUtils.generateETagForTranslatedDocument( - hProjectIteration, id, hLocale); + hProjectIteration, docId, hLocale); ResponseBuilder response = request.evaluatePreconditions(etag); if (response != null) { return response.build(); } HDocument document = - documentDAO.getByDocIdAndIteration(hProjectIteration, id); + documentDAO.getByDocIdAndIteration(hProjectIteration, docId); if (document == null || document.isObsolete()) { return Response.status(Status.NOT_FOUND).build(); } @@ -205,12 +221,24 @@ public Response deleteTranslations(String idNoSlash, LocaleId locale) { public Response putTranslations(String idNoSlash, LocaleId locale, TranslationsResource messageBody, Set extensions, String merge) { + String id = RestUtil.convertFromDocumentURIId(idNoSlash); + return putTranslationsWithDocId(locale, messageBody, id, extensions, merge); + } + + @Override + public Response putTranslationsWithDocId(LocaleId locale, + TranslationsResource messageBody, String docId, Set extensions, + String merge) { // check security (cannot be on @Restrict as it refers to method // parameters) identity.checkPermission("modify-translation", this.localeServiceImpl.getByLocaleId(locale), this.getSecuredIteration().getProject()); log.debug("start put translations"); + if (StringUtils.isBlank(docId)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("missing id").build(); + } MergeType mergeType; try { mergeType = MergeType.valueOf(merge.toUpperCase()); @@ -218,13 +246,12 @@ public Response putTranslations(String idNoSlash, LocaleId locale, return Response.status(Status.BAD_REQUEST) .entity("bad merge type " + merge).build(); } - String id = URIHelper.convertFromDocumentURIId(idNoSlash); HProjectIteration hProjectIteration = projectIterationDAO.getBySlug(projectSlug, iterationSlug); HLocale hLocale = restSlugValidator.validateTargetLocale(locale, projectSlug, iterationSlug); EntityTag etag = eTagUtils.generateETagForTranslatedDocument( - hProjectIteration, id, hLocale); + hProjectIteration, docId, hLocale); ResponseBuilder response = request.evaluatePreconditions(etag); if (response != null) { return response.build(); @@ -233,12 +260,12 @@ public Response putTranslations(String idNoSlash, LocaleId locale, boolean assignCreditToUploader = false; // Translate List warnings = this.translationServiceImpl.translateAllInDoc( - projectSlug, iterationSlug, id, locale, messageBody, extensions, + projectSlug, iterationSlug, docId, locale, messageBody, extensions, mergeType, assignCreditToUploader, TranslationSourceType.API_UPLOAD); // Regenerate etag in case it has changed etag = eTagUtils.generateETagForTranslatedDocument(hProjectIteration, - id, hLocale); + docId, hLocale); log.debug("successful put translation"); // TODO lastChanged StringBuilder sb = new StringBuilder(); diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java index a8a30f771a..643a5744f2 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java @@ -22,6 +22,7 @@ import java.io.InputStream; import java.io.Serializable; +import java.util.Optional; import java.util.concurrent.Future; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -55,7 +56,6 @@ import org.zanata.service.LockManagerService; import org.zanata.tmx.TMXParser; import org.zanata.util.CloseableIterator; -import com.google.common.base.Optional; // TODO this should use transactions (probably too big for one though) // TODO options to export obsolete docs and textflows to TMX? @@ -87,20 +87,27 @@ public class TranslationMemoryResourceService @Override @CheckRole("admin") - public Response getAllTranslationMemory(@Nullable LocaleId locale) { + public Response getAllTranslationMemory(@Nullable LocaleId srcLocale, + @Nullable LocaleId locale) { log.debug("exporting TMX for all projects, locale {}", locale); + if (srcLocale != null) { + localeServiceImpl.validateSourceLocale(srcLocale); + // TODO findTextFlowsByLocale + } if (locale != null) { localeServiceImpl.validateSourceLocale(locale); // TODO findTextFlowsByLocale } - String filename = makeTMXFilename(null, null, locale); - CloseableIterator iter = textFlowStreamDAO.findTextFlows(); - return buildTMX("getAllTranslationMemory", iter, locale, filename); + String filename = makeTMXFilename(null, null, srcLocale, locale); + CloseableIterator iter = textFlowStreamDAO.findTextFlows( + Optional.ofNullable(srcLocale)); + return buildTMX("getAllTranslationMemory", iter, srcLocale, locale, + filename); } @Override public Response getProjectTranslationMemory(@Nonnull String projectSlug, - @Nullable LocaleId locale) { + @Nullable LocaleId srcLocale, @Nullable LocaleId locale) { identity.checkPermission("", "download-tmx"); log.debug("exporting TMX for project {}, locale {}", projectSlug, locale); @@ -110,17 +117,17 @@ public Response getProjectTranslationMemory(@Nonnull String projectSlug, restSlugValidator.validateTargetLocale(locale, projectSlug); // TODO findTextFlowsByProjectAndLocale } - String filename = makeTMXFilename(projectSlug, null, locale); + String filename = makeTMXFilename(projectSlug, null, srcLocale, locale); CloseableIterator iter = - textFlowStreamDAO.findTextFlowsByProject(hProject); - return buildTMX("getProjectTranslationMemory-" + filename, iter, locale, - filename); + textFlowStreamDAO.findTextFlowsByProject(hProject, Optional.ofNullable(srcLocale)); + return buildTMX("getProjectTranslationMemory-" + filename, iter, + srcLocale, locale, filename); } @Override public Response getProjectIterationTranslationMemory( @Nonnull String projectSlug, @Nonnull String iterationSlug, - @Nullable LocaleId locale) { + @Nullable LocaleId srcLocale, @Nullable LocaleId locale) { identity.checkPermission("", "download-tmx"); log.debug("exporting TMX for project {}, iteration {}, locale {}", projectSlug, iterationSlug, locale); @@ -131,11 +138,12 @@ public Response getProjectIterationTranslationMemory( iterationSlug); // TODO findTextFlowsByProjectIterationAndLocale } - String filename = makeTMXFilename(projectSlug, iterationSlug, locale); + String filename = + makeTMXFilename(projectSlug, iterationSlug, srcLocale, locale); CloseableIterator iter = textFlowStreamDAO - .findTextFlowsByProjectIteration(hProjectIteration); + .findTextFlowsByProjectIteration(hProjectIteration, Optional.ofNullable(srcLocale)); return buildTMX("getProjectIterationTranslationMemory-" + filename, - iter, locale, filename); + iter, srcLocale, locale, filename); } @Override @@ -238,9 +246,10 @@ private Lock lockTM(String slug) { private Response buildTMX(String jobName, @Nonnull CloseableIterator iter, - @Nullable LocaleId locale, @Nonnull String filename) { + @Nullable LocaleId srcLocale, @Nullable LocaleId locale, + @Nonnull String filename) { TMXStreamingOutput output = new TMXStreamingOutput(jobName, - iter, new TranslationsTMXExportStrategy(locale)); + iter, new TranslationsTMXExportStrategy(srcLocale, locale)); return okResponse(filename, output); } @@ -261,11 +270,13 @@ private Response okResponse(String filename, StreamingOutput output) { @Nonnull private static String makeTMXFilename(@Nullable String projectSlug, - @Nullable String iterationSlug, @Nullable LocaleId locale) { + @Nullable String iterationSlug, @Nullable LocaleId srcLocale, + @Nullable LocaleId locale) { String p = projectSlug != null ? projectSlug : "allProjects"; String i = iterationSlug != null ? iterationSlug : "allVersions"; + String sl = srcLocale != null ? srcLocale.getId() : "allLocales"; String l = locale != null ? locale.getId() : "allLocales"; - return "zanata-" + p + "-" + i + "-" + l + ".tmx"; + return "zanata-" + p + "-" + i + "-" + sl + "-" + l + ".tmx"; } @Nonnull diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationsTMXExportStrategy.java b/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationsTMXExportStrategy.java index 012f1e052c..ecc68d8962 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationsTMXExportStrategy.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/TranslationsTMXExportStrategy.java @@ -60,6 +60,8 @@ public InvalidContentsException(String msg) { private static final String creationToolVersion = VersionUtility .getVersionInfo(TranslationsTMXExportStrategy.class).getVersionNo(); @Nullable + private final LocaleId srcLocaleId; + @Nullable private final LocaleId localeId; /** @@ -68,7 +70,9 @@ public InvalidContentsException(String msg) { * @param localeId * locale to export, or null for all locales */ - public TranslationsTMXExportStrategy(@Nullable LocaleId localeId) { + public TranslationsTMXExportStrategy(@Nullable LocaleId srcLocaleId, + @Nullable LocaleId localeId) { + this.srcLocaleId = srcLocaleId; this.localeId = localeId; } @@ -81,7 +85,9 @@ public Element buildHeader() throws IOException { header.addAttribute(new Attribute("segtype", "block")); header.addAttribute(new Attribute("o-tmf", "unknown")); header.addAttribute(new Attribute("adminlang", "en")); - header.addAttribute(new Attribute("srclang", TMXConstants.ALL_LOCALE)); + header.addAttribute(new Attribute("srclang", + srcLocaleId != null ? srcLocaleId.getId() : + TMXConstants.ALL_LOCALE)); header.addAttribute(new Attribute("datatype", "unknown")); return header; } diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/URIHelper.java b/server/zanata-war/src/main/java/org/zanata/rest/service/URIHelper.java deleted file mode 100644 index f49a4d43d2..0000000000 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/URIHelper.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.zanata.rest.service; - -import org.zanata.rest.RestUtil; - -/** - * @see {@link ZPathService} - */ -public final class URIHelper { - - private URIHelper() { - } - - public static String getProject(String projectSlug) { - return "/projects/p/" + projectSlug; - } - - public static String getIteration(String projectSlug, String iterationSlug) { - return getProject(projectSlug) + "/iterations/i/" + iterationSlug; - } - - public static String getDocument(String projectSlug, String iterationSlug, - String documentId) { - return getIteration(projectSlug, iterationSlug) + "/r/" - + RestUtil.convertToDocumentURIId(documentId); - } - - public static String convertFromDocumentURIId(String uriId) { - return uriId.replace(',', '/'); - } - - /** - * @deprecated Use {@link RestUtil#convertToDocumentURIId(String)} instead - */ - public static String convertToDocumentURIId(String id) { - return RestUtil.convertToDocumentURIId(id); - } - -} diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/ZPathService.java b/server/zanata-war/src/main/java/org/zanata/rest/service/ZPathService.java index ad003ca916..8d3d825dd6 100644 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/ZPathService.java +++ b/server/zanata-war/src/main/java/org/zanata/rest/service/ZPathService.java @@ -21,20 +21,11 @@ package org.zanata.rest.service; import java.io.Serializable; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.text.MessageFormat; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.inject.Inject; import javax.inject.Named; -import org.zanata.dao.ProjectDAO; import org.zanata.model.HDocument; -import org.zanata.model.HProject; import org.zanata.model.HProjectIteration; -import org.zanata.model.validator.SlugValidator; -import org.zanata.rest.RestUtil; /** * Service that provides static services to build, parse and interpret Zanata @@ -49,12 +40,6 @@ // TODO this should probably be Transactional (and not Dependent) public class ZPathService implements Serializable { - /* - * Public ZPaths. Used for rest resource path declaration. - */ - public static final String PROJECT_ZPATH = "/proj/" - + RestConstants.SLUG_PATTERN; - /* * Private ZPaths. Mainly used for generation. */ @@ -62,31 +47,7 @@ public class ZPathService implements Serializable { private static final String PROJECT_ITER_ZPATH_PRIVATE = PROJECT_ZPATH_PRIVATE + "/iterations/i/{1}"; private static final String DOCUMENT_ZPATH_PRIVATE = - PROJECT_ITER_ZPATH_PRIVATE + "/r/{2}"; - - /* - * Internal utilities based on the public ZPaths - */ - private static final Pattern PROJECT_ZPATH_PATTERN = Pattern - .compile(PROJECT_ZPATH); - - @Inject - private ProjectDAO projectDAO; - - public HProject resolveProject(String zPath) { - Matcher projMatcher = PROJECT_ZPATH_PATTERN.matcher(zPath); - if (projMatcher.matches()) { - String projectSlug = projMatcher.group(1); // Group 1, project slug - return projectDAO.getBySlug(projectSlug); - } else { - return null; - } - } - - public String generatePathForProject(HProject project) { - MessageFormat mssgFormat = new MessageFormat(PROJECT_ZPATH_PRIVATE); - return mssgFormat.format(project.getSlug()); - } + PROJECT_ITER_ZPATH_PRIVATE + "/resource?docId={2}"; public String generatePathForProjectIteration(HProjectIteration iteration) { MessageFormat mssgFormat = @@ -96,27 +57,10 @@ public String generatePathForProjectIteration(HProjectIteration iteration) { } public String generatePathForDocument(HDocument document) { - String docIdNoSlash = null; - try { - docIdNoSlash = - URLEncoder.encode(RestUtil.convertToDocumentURIId(document - .getDocId()), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - MessageFormat mssgFormat = new MessageFormat(DOCUMENT_ZPATH_PRIVATE); - return mssgFormat.format(new Object[] { + return mssgFormat.format(new Object[]{ document.getProjectIteration().getProject().getSlug(), - document.getProjectIteration().getSlug(), docIdNoSlash }); - } - - public Object resolve(String zPath) { - Matcher projMatcher = PROJECT_ZPATH_PATTERN.matcher(zPath); - if (projMatcher.matches()) { - return resolveProject(zPath); - } - - return null; + document.getProjectIteration().getSlug(), document + .getDocId()}); } } diff --git a/server/zanata-war/src/main/java/org/zanata/rest/service/package-info.java b/server/zanata-war/src/main/java/org/zanata/rest/service/package-info.java deleted file mode 100644 index 7771b124ab..0000000000 --- a/server/zanata-war/src/main/java/org/zanata/rest/service/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package org.zanata.rest.service; diff --git a/server/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java b/server/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java index 5db4b87c6e..ce292187ef 100644 --- a/server/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java +++ b/server/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java @@ -506,8 +506,11 @@ public boolean canDownloadFiles(HProjectIteration projectIteration) { */ @GrantsPermission(actions = "update") public boolean canUpdateVersionGroup(HIterationGroup group) { - return isLoggedIn() - && authenticatedAccount.get().getPerson().isMaintainer(group); + if (isLoggedIn()) { + HPerson person = authenticatedAccount.get().getPerson(); + return group.getMaintainers().contains(person); + } + return false; } @GrantsPermission(actions = "insert") diff --git a/server/zanata-war/src/main/java/org/zanata/service/LocaleService.java b/server/zanata-war/src/main/java/org/zanata/service/LocaleService.java index d0cb1f9c52..bd0e8595f8 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/LocaleService.java +++ b/server/zanata-war/src/main/java/org/zanata/service/LocaleService.java @@ -33,6 +33,8 @@ import org.zanata.common.LocaleId; import org.zanata.exception.ZanataServiceException; import org.zanata.model.HLocale; +import org.zanata.model.HProject; +import org.zanata.model.HProjectIteration; import org.zanata.model.HLocaleMember; import org.zanata.model.HTextFlowTarget; import org.zanata.rest.dto.LanguageTeamSearchResult; @@ -92,12 +94,18 @@ HLocale validateLocaleByProject(@Nonnull LocaleId locale, // TODO I don't think this method is specifically about source languages HLocale validateSourceLocale(LocaleId locale) throws ZanataServiceException; + List + getSupportedLanguageByProject(@Nonnull HProject project); + List getTranslation(@Nonnull String project, @Nonnull String iterationSlug, String username); List getSupportedLanguageByProjectIteration( @Nonnull String projectSlug, @Nonnull String iterationSlug); + List getSupportedLanguageByProjectIteration( + @Nonnull HProjectIteration version); + List getSupportedLanguageByProject(@Nonnull String project); Map getGlobalLocaleItems(); diff --git a/server/zanata-war/src/main/java/org/zanata/service/MergeTranslationsService.java b/server/zanata-war/src/main/java/org/zanata/service/MergeTranslationsService.java index d91d96f821..368537b1b2 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/MergeTranslationsService.java +++ b/server/zanata-war/src/main/java/org/zanata/service/MergeTranslationsService.java @@ -52,4 +52,5 @@ Future startMergeTranslations(String sourceProjectSlug, */ int getTotalProgressCount(HProjectIteration sourceVersion, HProjectIteration targetVersion); + } diff --git a/server/zanata-war/src/main/java/org/zanata/service/TransMemoryMergeService.java b/server/zanata-war/src/main/java/org/zanata/service/TransMemoryMergeService.java index 71741972cd..3119db01ee 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/TransMemoryMergeService.java +++ b/server/zanata-war/src/main/java/org/zanata/service/TransMemoryMergeService.java @@ -24,7 +24,14 @@ import java.util.List; import java.util.concurrent.Future; +import org.zanata.async.handle.MergeTranslationsTaskHandle; import org.zanata.async.handle.TransMemoryMergeTaskHandle; +import org.zanata.model.HLocale; +import org.zanata.model.HProjectIteration; +import org.zanata.model.HTextFlow; +import org.zanata.rest.dto.VersionTMMerge; +import org.zanata.webtrans.shared.model.ProjectIterationId; +import org.zanata.webtrans.shared.rest.dto.HasTMMergeCriteria; import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeRequest; public interface TransMemoryMergeService extends Serializable { @@ -33,6 +40,23 @@ List executeMerge( TransMemoryMergeRequest request, TransMemoryMergeTaskHandle asyncTaskHandle); + /** + * TM merge for a single document + * @param request + * @param asyncTaskHandle + * @return + */ Future> executeMergeAsync(TransMemoryMergeRequest request, TransMemoryMergeTaskHandle asyncTaskHandle); + + /** + * TM merge for a project version + * @param versionId + * @param mergeRequest + * @param handle + * @return + */ + Future startMergeTranslations(Long versionId, + VersionTMMerge mergeRequest, + MergeTranslationsTaskHandle handle); } diff --git a/server/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java b/server/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java index 02f421e2c2..ace0d38e5a 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java +++ b/server/zanata-war/src/main/java/org/zanata/service/TranslationMemoryService.java @@ -52,12 +52,14 @@ Optional searchBestMatchTransMemory(HTextFlow textFlow, * @param checkDocument * @param checkProject * @param thresholdPercent + * @param fromVersionIds * @return */ Optional searchBestMatchTransMemory( HTextFlow textFlow, LocaleId targetLocaleId, LocaleId sourceLocaleId, boolean checkContext, - boolean checkDocument, boolean checkProject, int thresholdPercent); + boolean checkDocument, boolean checkProject, int thresholdPercent, + List fromVersionIds); List searchTransMemory(LocaleId targetLocaleId, LocaleId sourceLocaleId, TransMemoryQuery transMemoryQuery); diff --git a/server/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java b/server/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java index c84ff9fd75..853a92e53e 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java +++ b/server/zanata-war/src/main/java/org/zanata/service/impl/LocaleServiceImpl.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; + import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.enterprise.context.ApplicationScoped; @@ -53,6 +54,7 @@ import org.zanata.service.LocaleService; import org.zanata.servlet.annotations.AllJavaLocales; import org.zanata.util.ComparatorUtil; + import com.google.common.collect.Maps; import com.ibm.icu.util.ULocale; @@ -318,6 +320,15 @@ public List getSupportedLanguageByProjectIteration( return getSupportedLanguageByProject(projectSlug); } + @Override + public List getSupportedLanguageByProjectIteration( + @Nonnull HProjectIteration version) { + if (version.isOverrideLocales()) { + return new ArrayList<>(version.getCustomizedLocales()); + } + return getSupportedLanguageByProject(version.getProject()); + } + @Override public List getSupportedLanguageByProject(@Nonnull String projectSlug) { @@ -328,6 +339,15 @@ public List getSupportedLanguageByProjectIteration( return localeDAO.findAllActiveAndEnabledByDefault(); } + @Override + public List + getSupportedLanguageByProject(@Nonnull HProject project) { + if (project.isOverrideLocales()) { + return new ArrayList<>(project.getCustomizedLocales()); + } + return localeDAO.findAllActiveAndEnabledByDefault(); + } + @Override public List getTranslation(@Nonnull String project, @Nonnull String iterationSlug, String username) { diff --git a/server/zanata-war/src/main/java/org/zanata/service/impl/MergeTranslationsServiceImpl.java b/server/zanata-war/src/main/java/org/zanata/service/impl/MergeTranslationsServiceImpl.java index 9d4cbd796a..83fcddb65d 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/impl/MergeTranslationsServiceImpl.java +++ b/server/zanata-war/src/main/java/org/zanata/service/impl/MergeTranslationsServiceImpl.java @@ -20,22 +20,26 @@ */ package org.zanata.service.impl; +import static org.zanata.events.TextFlowTargetStateEvent.TextFlowTargetStateChange; +import static org.zanata.transaction.TransactionUtilImpl.runInTransaction; + import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.Future; +import java.util.stream.Collectors; + import javax.annotation.Nonnull; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Multimap; import javax.enterprise.context.RequestScoped; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.inject.Named; + import org.zanata.async.Async; import org.zanata.async.AsyncTaskResult; import org.zanata.async.handle.MergeTranslationsTaskHandle; import org.zanata.common.ContentState; +import org.zanata.common.EntityStatus; import org.zanata.dao.ProjectIterationDAO; import org.zanata.dao.TextFlowDAO; import org.zanata.events.DocStatsEvent; @@ -47,19 +51,24 @@ import org.zanata.model.HSimpleComment; import org.zanata.model.HTextFlow; import org.zanata.model.HTextFlowTarget; +import org.zanata.model.ModelEntityBase; import org.zanata.model.type.TranslationSourceType; +import org.zanata.rest.dto.VersionTMMerge; import org.zanata.security.ZanataIdentity; import org.zanata.security.annotations.Authenticated; import org.zanata.service.LocaleService; import org.zanata.service.MergeTranslationsService; +import org.zanata.service.TransMemoryMergeService; import org.zanata.service.TranslationStateCache; import org.zanata.service.VersionStateCache; import org.zanata.util.TranslationUtil; + import com.google.common.base.Optional; import com.google.common.base.Stopwatch; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; -import static org.zanata.events.TextFlowTargetStateEvent.TextFlowTargetStateChange; -import static org.zanata.transaction.TransactionUtilImpl.runInTransaction; +import com.google.common.collect.Multimap; // Not @Transactional, because we use runInTransaction /** @@ -80,8 +89,6 @@ public class MergeTranslationsServiceImpl implements MergeTranslationsService { @Inject private TextFlowDAO textFlowDAO; @Inject - private ZanataIdentity identity; - @Inject private VersionStateCache versionStateCacheImpl; @Inject private TranslationStateCache translationStateCacheImpl; @@ -302,12 +309,7 @@ private void mergeTextFlowTarget(HTextFlowTarget sourceTft, */ private boolean isVersionsEmpty(HProjectIteration sourceVersion, HProjectIteration targetVersion) { - if (sourceVersion.getDocuments().isEmpty()) { - log.error("No documents in source version {}:{}", - sourceVersion.getProject().getSlug(), - sourceVersion.getSlug()); - return true; - } + if (isVersionEmpty(sourceVersion)) return true; if (targetVersion.getDocuments().isEmpty()) { log.error("No documents in target version {}:{}", targetVersion.getProject().getSlug(), @@ -317,11 +319,21 @@ private boolean isVersionsEmpty(HProjectIteration sourceVersion, return false; } + private boolean isVersionEmpty(HProjectIteration version) { + if (version.getDocuments().isEmpty()) { + log.error("No documents in version {}:{}", + version.getProject().getSlug(), + version.getSlug()); + return true; + } + return false; + } + private void prepareMergeTranslationsHandle( @Nonnull HProjectIteration sourceVersion, @Nonnull HProjectIteration targetVersion, @Nonnull MergeTranslationsTaskHandle handle) { - handle.setTriggeredBy(identity.getAccountUsername()); + handle.setTriggeredBy(authenticatedAccount.getUsername()); int total = getTotalProgressCount(sourceVersion, targetVersion); handle.setMaxProgress(total); handle.setTotalTranslations(total); @@ -390,4 +402,5 @@ public static boolean shouldMerge(HTextFlowTarget sourceTft, return useNewerTranslation && sourceTft.getLastChanged().after(targetTft.getLastChanged()); } + } diff --git a/server/zanata-war/src/main/java/org/zanata/service/impl/TransMemoryMergeServiceImpl.java b/server/zanata-war/src/main/java/org/zanata/service/impl/TransMemoryMergeServiceImpl.java index fff7ec32c8..3f90267ba5 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/impl/TransMemoryMergeServiceImpl.java +++ b/server/zanata-war/src/main/java/org/zanata/service/impl/TransMemoryMergeServiceImpl.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.enterprise.context.RequestScoped; import javax.enterprise.event.Event; @@ -36,8 +38,11 @@ import org.slf4j.LoggerFactory; import org.zanata.async.Async; import org.zanata.async.AsyncTaskResult; +import org.zanata.async.handle.MergeTranslationsTaskHandle; import org.zanata.async.handle.TransMemoryMergeTaskHandle; import org.zanata.common.ContentState; +import org.zanata.common.EntityStatus; +import org.zanata.dao.ProjectIterationDAO; import org.zanata.dao.TextFlowDAO; import org.zanata.dao.TransMemoryUnitDAO; import org.zanata.events.TextFlowTargetUpdateContextEvent; @@ -45,16 +50,21 @@ import org.zanata.events.TransMemoryMergeProgressEvent; import org.zanata.model.HAccount; import org.zanata.model.HLocale; +import org.zanata.model.HProjectIteration; import org.zanata.model.HTextFlow; import org.zanata.model.HTextFlowTarget; +import org.zanata.model.ModelEntityBase; import org.zanata.model.tm.TransMemoryUnit; import org.zanata.model.type.EntityType; import org.zanata.model.type.TranslationSourceType; +import org.zanata.rest.dto.VersionTMMerge; +import org.zanata.security.ZanataIdentity; import org.zanata.security.annotations.Authenticated; import org.zanata.service.LocaleService; import org.zanata.service.TransMemoryMergeService; import org.zanata.service.TranslationMemoryService; import org.zanata.service.TranslationService; +import org.zanata.service.VersionStateCache; import org.zanata.transaction.TransactionUtil; import org.zanata.util.TransMemoryMergeStatusResolver; import org.zanata.util.TranslationUtil; @@ -63,17 +73,20 @@ import org.zanata.webtrans.shared.model.TransUnitId; import org.zanata.webtrans.shared.model.TransUnitUpdateRequest; import org.zanata.webtrans.shared.model.WorkspaceId; +import org.zanata.webtrans.shared.rest.dto.HasTMMergeCriteria; import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeRequest; import org.zanata.webtrans.shared.rpc.MergeRule; import org.zanata.webtrans.shared.rpc.TransUnitUpdated; import org.zanata.webtrans.shared.search.FilterConstraints; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; /** * @author Sean Flanigan * sflaniga@redhat.com + * @author Patrick Huang pahuang@redhat.com */ @Named("transMemoryMergeServiceImpl") @RequestScoped @@ -110,6 +123,15 @@ public class TransMemoryMergeServiceImpl implements TransMemoryMergeService { @Inject private TransactionUtil transactionUtil; + @Inject + private ProjectIterationDAO projectIterationDAO; + + @Inject + private ZanataIdentity identity; + + @Inject + private VersionStateCache versionStateCacheImpl; + @Inject @Authenticated private HAccount authenticatedAccount; @@ -137,7 +159,7 @@ public List executeMerge( startTime, authenticatedAccount.getUsername(), request.editorClientId, request.documentId, total)); - asyncTaskHandle.setTotalTextFlows(total); + asyncTaskHandle.setMaxProgress(total); asyncTaskHandle.setTMMergeTarget(request.projectIterationId, request.documentId, request.localeId); @@ -155,15 +177,24 @@ public List executeMerge( UNTRANSLATED_FILTER, index, BATCH_SIZE); int processedSize = textFlowsBatch.size(); index = index + processedSize; - asyncTaskHandle.setTextFlowFilled(index); + asyncTaskHandle.increaseProgress(processedSize); + Consumer + callback = + updateRequest -> textFlowTargetUpdateContextEvent + .fire(new TextFlowTargetUpdateContextEvent( + updateRequest.getTransUnitId(), + request.localeId, + request.editorClientId, + TransUnitUpdated.UpdateType.NonEditorSave)); List batchResult = - translateInBatch(request, textFlowsBatch, targetLocale); + translateInBatch(request, textFlowsBatch, targetLocale, + Collections.emptyList(), Optional.of(callback)); finalResult.addAll(batchResult); log.debug("TM merge handle: {}", asyncTaskHandle); transMemoryMergeProgressEvent .fire(new TransMemoryMergeProgressEvent(workspaceId, total, - asyncTaskHandle.getTextFlowFilled(), + asyncTaskHandle.getCurrentProgress(), request.editorClientId, request.documentId)); } } catch (Exception e) { @@ -190,20 +221,95 @@ public Future> executeMergeAsync(Tran return AsyncTaskResult.completed(translationResults); } + @Async + @Override + public Future startMergeTranslations(Long targetVersionId, + VersionTMMerge mergeRequest, + MergeTranslationsTaskHandle handle) { + // since this is async we need to reload entities + HProjectIteration targetVersion = + projectIterationDAO.findById(targetVersionId); + HLocale targetLocale = + localeServiceImpl.getByLocaleId(mergeRequest.getLocaleId()); + + List localesInTargetVersion = localeServiceImpl + .getSupportedLanguageByProjectIteration(targetVersion); + if (!localesInTargetVersion.contains(targetLocale)) { + log.error("No locales enabled in target version of [{}]", + targetVersion.userFriendlyToString()); + return AsyncTaskResult.completed(); + } + + List fromVersionIds = mergeRequest.getFromProjectVersions().stream() + .map(projectIterationId -> projectIterationDAO.getBySlug( + projectIterationId.getProjectSlug(), + projectIterationId.getIterationSlug())) + .filter(ver -> ver != null + && ver.getStatus() != EntityStatus.OBSOLETE + && localeServiceImpl + .getSupportedLanguageByProjectIteration(ver) + .contains(targetLocale)) + .map(ModelEntityBase::getId).collect(Collectors.toList()); + + + long mergeTargetCount = textFlowDAO.getUntranslatedOrFuzzyTextFlowCountInVersion( + targetVersion.getId(), targetLocale); + + Optional + taskHandleOpt = Optional.ofNullable(handle); + if (taskHandleOpt.isPresent()) { + MergeTranslationsTaskHandle handle1 = taskHandleOpt.get(); + handle1.setTriggeredBy(identity.getAccountUsername()); + handle1.setMaxProgress((int) mergeTargetCount); + handle1.setTotalTranslations(mergeTargetCount); + } + Stopwatch overallStopwatch = Stopwatch.createStarted(); + log.info("merge translations from TM start: from {} to {}", + fromVersionIds, + targetVersion.userFriendlyToString()); + int startCount = 0; + + while (startCount < mergeTargetCount) { + List batch = textFlowDAO + .getUntranslatedOrFuzzyTextFlowsInVersion( + targetVersion.getId(), targetLocale, startCount, + BATCH_SIZE); + translateInBatch(mergeRequest, batch, + targetLocale, fromVersionIds, Optional.empty()); + + taskHandleOpt.ifPresent( + mergeTranslationsTaskHandle -> mergeTranslationsTaskHandle + .increaseProgress(batch.size())); + startCount += BATCH_SIZE; + } + versionStateCacheImpl.clearVersionStatsCache(targetVersion.getId()); + log.info("merge translation from TM end: from {} to {}, {}", + mergeRequest.getFromProjectVersions(), + targetVersion.userFriendlyToString(), overallStopwatch); + return AsyncTaskResult.completed(); + } + /** * This method will run in transaction and manages its own transaction. * * @param request - * TM merge request + * TM merge request criteria * @param textFlows * the text flows to be filled * @param targetLocale * target locale + * @param fromVersionIds + * source version ids + * @param callbackOnUpdate + * an optional callback to call when we have a + * TransUnitUpdateRequest ready * @return translation results */ private List translateInBatch( - TransMemoryMergeRequest request, List textFlows, - HLocale targetLocale) { + HasTMMergeCriteria request, List textFlows, + HLocale targetLocale, + List fromVersionIds, + Optional> callbackOnUpdate) { if (textFlows.isEmpty()) { return Collections.emptyList(); @@ -226,7 +332,8 @@ private List translateInBatch( hTextFlow, targetLocale.getLocaleId(), hTextFlow.getDocument().getLocale().getLocaleId(), checkContext, checkDocument, checkProject, - request.getThresholdPercent()); + request.getThresholdPercent(), + fromVersionIds); if (tmResult.isPresent()) { TransUnitUpdateRequest updateRequest = createRequest(request, targetLocale, @@ -234,17 +341,12 @@ private List translateInBatch( if (updateRequest != null) { updateRequests.add(updateRequest); - textFlowTargetUpdateContextEvent - .fire(new TextFlowTargetUpdateContextEvent( - updateRequest.getTransUnitId(), - request.localeId, - request.editorClientId, - TransUnitUpdated.UpdateType.NonEditorSave)); + callbackOnUpdate.ifPresent(c -> c.accept(updateRequest)); } } } return translationServiceImpl.translate( - request.localeId, updateRequests); + targetLocale.getLocaleId(), updateRequests); }); } catch (Exception e) { log.error("exception during TM merge", e); @@ -252,7 +354,7 @@ private List translateInBatch( } } - private TransUnitUpdateRequest createRequest(TransMemoryMergeRequest action, + private TransUnitUpdateRequest createRequest(HasTMMergeCriteria action, HLocale hLocale, HTextFlow hTextFlowToBeFilled, TransMemoryResultItem tmResult, HTextFlowTarget oldTarget) { diff --git a/server/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java b/server/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java index 4ee741a477..3119974a0b 100644 --- a/server/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java +++ b/server/zanata-war/src/main/java/org/zanata/service/impl/TranslationMemoryServiceImpl.java @@ -21,6 +21,24 @@ package org.zanata.service.impl; import static com.google.common.collect.Collections2.filter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; + import org.apache.commons.lang.StringUtils; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.index.Term; @@ -36,9 +54,6 @@ import org.apache.lucene.search.WildcardQuery; import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.FullTextQuery; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.inject.Named; import org.zanata.common.ContentState; import org.zanata.common.EntityStatus; import org.zanata.common.LocaleId; @@ -67,21 +82,12 @@ import org.zanata.webtrans.shared.model.TransMemoryResultItem; import org.zanata.webtrans.shared.rpc.HasSearchType; import org.zanata.webtrans.shared.rpc.LuceneQuery; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nonnull; /** * @author Alex Eng aeng@redhat.com @@ -155,6 +161,7 @@ public TransMemoryDetails getTransMemoryDetail(HLocale hLocale, } /** + * TODO this is only used by test. Should we remove it? * This is used by CopyTrans, with ContentHash search in lucene. Returns * first entry of the matches which sort by HTextFlowTarget.lastChanged DESC * @@ -172,7 +179,8 @@ public Optional searchBestMatchTransMemory( boolean checkDocument, boolean checkProject) { TransMemoryQuery query = buildTMQuery(textFlow, HasSearchType.SearchType.CONTENT_HASH, - checkContext, checkDocument, checkProject, false); + checkContext, checkDocument, checkProject, false, + Collections.emptyList()); Collection matches = findMatchingTranslation(targetLocaleId, sourceLocaleId, query, 0, Optional.empty(), HTextFlowTarget.class); @@ -185,7 +193,6 @@ public Optional searchBestMatchTransMemory( /** * This is used by TMMerge. Returns first entry of the matches which sort by * similarityPercent, sourceContents, and contents size. - * * @param textFlow * @param targetLocaleId * @param sourceLocaleId @@ -193,15 +200,18 @@ public Optional searchBestMatchTransMemory( * @param checkDocument * @param checkProject * @param thresholdPercent + * @param fromVersionIds */ @Override public Optional searchBestMatchTransMemory( HTextFlow textFlow, LocaleId targetLocaleId, LocaleId sourceLocaleId, boolean checkContext, - boolean checkDocument, boolean checkProject, int thresholdPercent) { + boolean checkDocument, boolean checkProject, int thresholdPercent, + List fromVersionIds) { TransMemoryQuery query = buildTMQuery(textFlow, HasSearchType.SearchType.FUZZY_PLURAL, - checkContext, checkDocument, checkProject, true); + checkContext, checkDocument, checkProject, + true, fromVersionIds); List tmResults = searchTransMemory(targetLocaleId, sourceLocaleId, query); // findTMAboveThreshold @@ -232,7 +242,7 @@ public List searchTransMemory( } List results = Lists.newArrayList(matchesMap.values()); - Collections.sort(results, TransMemoryResultComparator.COMPARATOR); + Collections.sort(results, new TransMemoryResultComparator(transMemoryQuery.getFromVersionIds())); return results; } @@ -248,7 +258,8 @@ public List searchTransMemoryWithDetails( private TransMemoryQuery buildTMQuery(HTextFlow textFlow, HasSearchType.SearchType searchType, boolean checkContext, boolean checkDocument, boolean checkProject, - boolean includeOwnTranslation) { + boolean includeOwnTranslation, + List fromVersions) { TransMemoryQuery.Condition project = new TransMemoryQuery.Condition( checkProject, textFlow.getDocument().getProjectIteration() .getProject().getId().toString()); @@ -262,7 +273,7 @@ private TransMemoryQuery buildTMQuery(HTextFlow textFlow, project, document, res); } else { query = new TransMemoryQuery(textFlow.getContents(), searchType, - project, document, res); + project, document, res, fromVersions); } if (!includeOwnTranslation) { query.setIncludeOwnTranslation(false, textFlow.getId().toString()); @@ -339,9 +350,12 @@ private void processIndexMatch(TransMemoryQuery transMemoryQuery, textFlowContents, MINIMUM_SIMILARITY); return; } + Long fromVersionId = textFlowTarget.getTextFlow().getDocument() + .getProjectIteration().getId(); TransMemoryResultItem item = createOrGetResultItem(matchesMap, match, matchType, - textFlowContents, targetContents, percent); + textFlowContents, targetContents, percent, + fromVersionId); addTextFlowTargetToResultMatches(textFlowTarget, item); } else if (entity instanceof TransMemoryUnit) { TransMemoryUnit transUnit = (TransMemoryUnit) entity; @@ -360,7 +374,7 @@ private void processIndexMatch(TransMemoryQuery transMemoryQuery, } TransMemoryResultItem item = createOrGetResultItem(matchesMap, match, TransMemoryResultItem.MatchType.Imported, - sourceContents, targetContents, percent); + sourceContents, targetContents, percent, null); addTransMemoryUnitToResultMatches(item, transUnit); } } @@ -422,13 +436,13 @@ private TransMemoryResultItem createOrGetResultItem( Map matchesMap, Object[] match, TransMemoryResultItem.MatchType matchType, ArrayList sourceContents, ArrayList targetContents, - double percent) { + double percent, Long fromVersionId) { TMKey key = new TMKey(sourceContents, targetContents); TransMemoryResultItem item = matchesMap.get(key); if (item == null) { float score = (Float) match[0]; item = new TransMemoryResultItem(sourceContents, targetContents, - matchType, score, percent); + matchType, score, percent, fromVersionId); matchesMap.put(key, item); } return item; @@ -465,9 +479,17 @@ private void addTextFlowTargetToResultMatches( * NB just because this Comparator returns 0 doesn't mean the matches are * identical. */ - private static enum TransMemoryResultComparator - implements Comparator { - COMPARATOR; + private static class TransMemoryResultComparator + implements Comparator, Serializable { + + + private static final long serialVersionUID = 1L; + private final List fromVersionIds; + + public TransMemoryResultComparator( + @Nullable List fromVersionIds) { + this.fromVersionIds = fromVersionIds; + } @Override public int compare(TransMemoryResultItem m1, TransMemoryResultItem m2) { @@ -483,7 +505,37 @@ public int compare(TransMemoryResultItem m1, TransMemoryResultItem m2) { // sort longer string lists first (more plural forms) return result; } - return m2.getMatchType().compareTo(m1.getMatchType()); + result = m2.getMatchType().compareTo(m1.getMatchType()); + if (result != 0) { + // sort match type + return result; + } + // if TM is from TMX, getFromVersionId is null + // if fromVersionIds is empty, we have no restriction on source version + if (m2.getFromVersionId() != null && + m1.getFromVersionId() != null && fromVersionIds != null && + !fromVersionIds.isEmpty()) { + int indexOfM2 = fromVersionIds.indexOf(m2.getFromVersionId()); + int indexOfM1 = fromVersionIds.indexOf(m1.getFromVersionId()); + // sort higher when index is lower + // if index is -1, something wrong with our lucene query or index + if (indexOfM1 < 0 || indexOfM2 < 0) { + log.warn("Having TM result not from requested source versions:{}", fromVersionIds); + if (indexOfM1 < 0 && indexOfM2 >= 0) { + // m2 rank higher since it's from the defined source versions + return 1; + } else if (indexOfM2 < 0 && indexOfM1 >= 0) { + // m1 is from defined source versions + return -1; + } else { + // they are both not from defined source versions + return result; + } + + } + return Integer.compare(indexOfM1, indexOfM2); + } + return result; } private int compare(List list1, List list2) { @@ -780,6 +832,18 @@ private void buildContextQuery(BooleanQuery query, query.add(resIdQuery, BooleanClause.Occur.SHOULD); } } + if (queryParams.getFromVersionIds() != null && !queryParams.getFromVersionIds().isEmpty()) { + BooleanQuery.Builder fromVersions = new BooleanQuery.Builder(); + queryParams.getFromVersionIds().forEach(projectIterationId -> { + TermQuery fromVersionQuery = new TermQuery( + new Term(IndexFieldLabels.PROJECT_VERSION_ID_FIELD, + projectIterationId.toString())); + + + fromVersions.add(fromVersionQuery, BooleanClause.Occur.SHOULD); + }); + query.add(fromVersions.build(), BooleanClause.Occur.MUST); + } } private Query buildContentQuery(TransMemoryQuery query, diff --git a/server/zanata-war/src/main/java/org/zanata/servlet/UrlRewriteConfig.java b/server/zanata-war/src/main/java/org/zanata/servlet/UrlRewriteConfig.java index f4fa67b2f8..895d67bb3b 100644 --- a/server/zanata-war/src/main/java/org/zanata/servlet/UrlRewriteConfig.java +++ b/server/zanata-war/src/main/java/org/zanata/servlet/UrlRewriteConfig.java @@ -138,17 +138,6 @@ public Configuration getConfiguration(final ServletContext context) { .addRule(Join.path("/iteration/view/{projectSlug}/{iterationSlug}/{section}").to("/iteration/view.xhtml")) .where("section").matches(".*") - /* JSF serves zanata-assets with suffix of .xhtml only. - This is to make sure any reference to zanata-assets - without .xhtml can access the resource. - e.g. jars/assets/style.css forwards to - jars/assets/style.css.xhtml - */ - .addRule(Join.pathNonBinding("/javax.faces.resource/jars/assets/{path}") - .to("/javax.faces.resource/jars/assets/{path}.xhtml")) - .when(Direction.isInbound()) - .where("path").matches(".*(?() { @Override public int compare(HLocale hLocale, HLocale hLocale2) { - return hLocale.retrieveDisplayName().compareTo( + return hLocale.retrieveDisplayName().compareToIgnoreCase( hLocale2.retrieveDisplayName()); } }; diff --git a/server/zanata-war/src/main/java/org/zanata/util/TransMemoryMergeStatusResolver.java b/server/zanata-war/src/main/java/org/zanata/util/TransMemoryMergeStatusResolver.java index 42b7048bf4..d8c0ea1617 100644 --- a/server/zanata-war/src/main/java/org/zanata/util/TransMemoryMergeStatusResolver.java +++ b/server/zanata-war/src/main/java/org/zanata/util/TransMemoryMergeStatusResolver.java @@ -26,7 +26,7 @@ import org.zanata.model.HTextFlowTarget; import org.zanata.webtrans.shared.model.TransMemoryDetails; import org.zanata.webtrans.shared.model.TransMemoryResultItem; -import org.zanata.webtrans.shared.rest.dto.TransMemoryMergeRequest; +import org.zanata.webtrans.shared.rest.dto.HasTMMergeCriteria; import org.zanata.webtrans.shared.rpc.MergeRule; import com.google.common.base.Objects; @@ -64,7 +64,7 @@ public static TransMemoryMergeStatusResolver newInstance() { * @return content state to be set on auto translated target. If null means * we want to reject the auto translation via TM merge */ - public ContentState decideStatus(TransMemoryMergeRequest action, + public ContentState decideStatus(HasTMMergeCriteria action, HTextFlow tfToBeFilled, TransMemoryDetails tmDetail, TransMemoryResultItem tmResult, HTextFlowTarget oldTarget) { @@ -101,7 +101,7 @@ public ContentState decideStatus(TransMemoryMergeRequest action, * @return content state to be set on auto translated target. If null means * we want to reject the auto translation via TM merge */ - public ContentState decideStatus(TransMemoryMergeRequest action, + public ContentState decideStatus(HasTMMergeCriteria action, TransMemoryResultItem tmResult, HTextFlowTarget oldTarget) { if ((int) tmResult.getSimilarityPercent() != 100) { @@ -123,14 +123,14 @@ public ContentState decideStatus(TransMemoryMergeRequest action, return ContentState.Translated; } - private void compareTextFlowResId(TransMemoryMergeRequest action, + private void compareTextFlowResId(HasTMMergeCriteria action, HTextFlow tfToBeFilled, TransMemoryDetails tmDetail) { if (notEqual(tfToBeFilled.getResId(), tmDetail.getResId())) { setFlagsBasedOnOption(action.getDifferentContextRule()); } } - private void compareTextFlowMsgContext(TransMemoryMergeRequest action, + private void compareTextFlowMsgContext(HasTMMergeCriteria action, HTextFlow tfToBeFilled, TransMemoryDetails tmDetail) { String msgCtx = null; if (tfToBeFilled.getPotEntryData() != null) { @@ -141,7 +141,7 @@ private void compareTextFlowMsgContext(TransMemoryMergeRequest action, } } - private void compareDocId(TransMemoryMergeRequest action, HTextFlow tfToBeFilled, + private void compareDocId(HasTMMergeCriteria action, HTextFlow tfToBeFilled, TransMemoryDetails tmDetail) { if (notEqual(tfToBeFilled.getDocument().getDocId(), tmDetail.getDocId())) { @@ -149,7 +149,7 @@ private void compareDocId(TransMemoryMergeRequest action, HTextFlow tfToBeFilled } } - private void compareProjectName(TransMemoryMergeRequest action, + private void compareProjectName(HasTMMergeCriteria action, HTextFlow tfToBeFilled, TransMemoryDetails tmDetail) { if (notEqual(tfToBeFilled.getDocument().getProjectIteration() .getProject().getName(), tmDetail.getProjectName())) { diff --git a/server/zanata-war/src/main/java/org/zanata/util/UrlUtil.java b/server/zanata-war/src/main/java/org/zanata/util/UrlUtil.java index 720f053c2d..abf6c74dae 100644 --- a/server/zanata-war/src/main/java/org/zanata/util/UrlUtil.java +++ b/server/zanata-war/src/main/java/org/zanata/util/UrlUtil.java @@ -138,6 +138,10 @@ public String projectUrl(String projectSlug) { return contextPath + "/project/view/" + projectSlug + dswidQuery; } + public String groupUrl(String groupSlug) { + return contextPath + "/version-group/view/" + groupSlug + dswidQuery; + } + /** * Get add-version url with dswid parameter */ diff --git a/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/DownloadAllFilesHandler.java b/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/DownloadAllFilesHandler.java index cfc1b56e98..2c5ca66af0 100644 --- a/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/DownloadAllFilesHandler.java +++ b/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/DownloadAllFilesHandler.java @@ -27,6 +27,7 @@ import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.inject.Named; + import org.zanata.async.AsyncTaskHandle; import org.zanata.async.AsyncTaskHandleManager; import org.zanata.dao.ProjectIterationDAO; @@ -72,7 +73,7 @@ public DownloadAllFilesResult execute(DownloadAllFilesAction action, action.getVersionSlug()); if (identity.hasPermission(version, "download-all")) { AsyncTaskHandle handle = new AsyncTaskHandle(); - Serializable taskKey = + String keyId = asyncTaskHandleManager.registerTaskHandle(handle); // TODO This should be in a service and share code with the JSF // pages that do the same thing @@ -85,9 +86,7 @@ public DownloadAllFilesResult execute(DownloadAllFilesAction action, throw new ActionException(e); } - // NB Keys are currently strings, but this is tied to the - // implementation - return new DownloadAllFilesResult(true, taskKey.toString()); + return new DownloadAllFilesResult(true, keyId); } return new DownloadAllFilesResult(false, null); diff --git a/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/GetDownloadAllFilesProgressHandler.java b/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/GetDownloadAllFilesProgressHandler.java index 7e2bd9400b..4430a0c003 100644 --- a/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/GetDownloadAllFilesProgressHandler.java +++ b/server/zanata-war/src/main/java/org/zanata/webtrans/server/rpc/GetDownloadAllFilesProgressHandler.java @@ -52,12 +52,12 @@ public class GetDownloadAllFilesProgressHandler public GetDownloadAllFilesProgressResult execute( GetDownloadAllFilesProgress action, ExecutionContext context) throws ActionException { - int currentProgress = 0; - int maxProgress = 0; + long currentProgress = 0; + long maxProgress = 0; String downloadId = ""; AsyncTaskHandle handle = - asyncTaskHandleManager.getHandleByKey(action.getProcessId()); + asyncTaskHandleManager.getHandleByKeyId(action.getProcessId()); if (handle != null) { if (handle.isDone()) { try { diff --git a/server/zanata-war/src/main/resources/db/changelogs/db.changelog-4.2.xml b/server/zanata-war/src/main/resources/db/changelogs/db.changelog-4.2.xml index 2c02b29489..94248aee0e 100644 --- a/server/zanata-war/src/main/resources/db/changelogs/db.changelog-4.2.xml +++ b/server/zanata-war/src/main/resources/db/changelogs/db.changelog-4.2.xml @@ -159,4 +159,10 @@ + + Alter TransMemory description, limit max length + UPDATE TransMemory SET description=SUBSTRING(description,1,100); + + diff --git a/server/zanata-war/src/main/resources/messages.properties b/server/zanata-war/src/main/resources/messages.properties index e2680de99c..ec38198fff 100644 --- a/server/zanata-war/src/main/resources/messages.properties +++ b/server/zanata-war/src/main/resources/messages.properties @@ -331,6 +331,7 @@ jsf.project.WebhookName=Webhook name jsf.project.WebhookType.label=Type jsf.project.InvalidUrl=Invalid URL: {0} jsf.project.DuplicateUrl=Same URL is already in the list. {0} +jsf.project.name.validation.alphanumeric=Project name must contain at least one alphanumeric character jsf.webhook.response.state={0}% {1} jsf.webhook.test.label=Test webhook jsf.webhook.test.tooltip=Fire a test event @@ -375,7 +376,7 @@ jsf.NoPeople=No people are assigned to this project jsf.project.RoleRestrictions=Role Restrictions jsf.project.ProjectRestrictedToFollowingRoles=This project has restricted access for the following User roles: jsf.ExportTMXProject=Export project to TMX -jsf.ConfirmExportTMXProject=Are you sure you want to export this project to TMX? +jsf.iteration.ExportTMXProjectInfo=Export all translations in this project to TMX jsf.project.SourceCode=Source Code jsf.project.HomePage=Home Page jsf.project.Repository=Repository @@ -652,6 +653,7 @@ jsf.pager.NextPage=Next jsf.pager.PreviousPage=Previous jsf.iteration.ExportTMXIter=Export Version to TMX +jsf.iteration.ExportTMXIterInfo=Export all translations in this version to TMX jsf.iteration.ExportTMX.Language=Export {0} Documents to TMX jsf.iteration.files.NoFiles=No Files Available @@ -741,6 +743,7 @@ jsf.iteration.mergeTrans.noSourceAndTarget=No source version or target version. jsf.iteration.mergeTrans.cancelMerge=Cancel merge translations jsf.iteration.mergeTrans.noVersionToMerge.heading=No versions available in this project to merge from. jsf.iteration.mergeTrans.noVersionToMerge.description=Please choose another project. +jsf.iteration.TMMerge=TM Merge Translations #------ [home] > Projects > [project-id] > [version-id] > Settings ------ jsf.upload.UploadNewDocuments=Upload new documents diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/dashboard/settings.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/dashboard/settings.xhtml index 75655e8e2f..41178b1411 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/dashboard/settings.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/dashboard/settings.xhtml @@ -277,13 +277,15 @@

    - - + and Maven plugin - - + .

    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/language/search_user_modal.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/language/search_user_modal.xhtml index 7854ec2101..a7d731dc9b 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/language/search_user_modal.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/language/search_user_modal.xhtml @@ -33,26 +33,30 @@ -
    - -
    + + + + + + + + +
    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/language/settings-tab.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/language/settings-tab.xhtml index 50eac089a2..567f2f3ca6 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/language/settings-tab.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/language/settings-tab.xhtml @@ -79,7 +79,7 @@ #{languageAction.examplePluralForms} - + #{msgs['jsf.ProjectName']} + value="#{projectHome.instance.name}" + valueChangeListener="#{projectHome.onProjectNameChange}"> + + @@ -91,9 +94,9 @@ class="heading--secondary l--push-all-0">#{msgs['jsf.ProjectType']}

    #{msgs['jsf.ProjectType.title']} - +

    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-translation.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-translation.xhtml index e8ac471f00..20964a4a8c 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-translation.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-translation.xhtml @@ -77,10 +77,10 @@

    #{msgs['jsf.CopyTrans']}

    #{msgs['jsf.Copytrans.message']} - - +

    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml index f3c3e2213f..66f4a71adf 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab-webhook.xhtml @@ -73,8 +73,9 @@ #{msgs['jsf.project.WebHooks']} - + diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml index f3b6c07bfa..481e9ba36a 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/project/settings-tab.xhtml @@ -119,9 +119,9 @@ + render="settings-permissions-form"> @@ -137,8 +137,8 @@ actionBean="#{projectHome.maintainerAutocomplete}" maxlength="80" minlength="3" id="maintainerAutocomplete" fetchValue="#{result.account.username}" - render="settings-permissions-form,maintainers-size, maintainers-form" - oncomplete="zanata.form.appendCheckboxes(getUserRoleId());focusCurrentActiveInput()" + render="settings-permissions-form" + oncomplete="resetProjectMembers();zanata.form.appendCheckboxes(getUserRoleId());focusCurrentActiveInput()" placeholder="#{msgs['jsf.SearchUsers']}"> diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/transmemory_edit_form.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/transmemory_edit_form.xhtml index 39a3f81cd5..3135aeb20d 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/transmemory_edit_form.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/transmemory_edit_form.xhtml @@ -2,8 +2,28 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:zanata="http://java.sun.com/jsf/composite/zanata" + xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" - xmlns:a4j="http://richfaces.org/a4j"> + xmlns:a4j="http://richfaces.org/a4j" + xmlns:rich="http://richfaces.org/rich"> + +
    #{msgs['jsf.transmemory.TransMemoryId']} @@ -24,7 +44,11 @@ #{msgs['jsf.Description']} + value="#{translationMemoryHome.instance.description}" + onkeyup="validateDescriptionField(this)"> + + +
    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/version-group/settings-tab.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/version-group/settings-tab.xhtml index 2838740e7f..f46e3fcff6 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/version-group/settings-tab.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/version-group/settings-tab.xhtml @@ -223,7 +223,6 @@ styleClass="button--link l--float-right reveal__target" action="#{versionGroupHome.removeMaintainer(maintainer)}" onbegin="jQuery('#remove-maintainer-#{status.index}').addClass('is-hidden')" - execute="@this" render="settings-maintainers-form,maintainers-size,maintainers-list,maintainers-form" title="#{msgs['jsf.group.RemoveMaintainer.title']}"> #{msgs['jsf.group.RemoveMaintainer.sr.label']} diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/version/edit_form.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/version/edit_form.xhtml index f2a75d52d3..fbfd8f0392 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/version/edit_form.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/version/edit_form.xhtml @@ -91,9 +91,9 @@ class="heading--secondary l--push-all-0">#{msgs['jsf.projectType']}

    #{msgs['jsf.ProjectType.title']} - +

    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/layout/version/settings-tab.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/layout/version/settings-tab.xhtml index 6db79dfd82..5a2b760377 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/layout/version/settings-tab.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/layout/version/settings-tab.xhtml @@ -259,10 +259,10 @@ value="requireTranslationReview"/>

    #{msgs['jsf.iteration.requireReview.message']} - + title="#{msgs['jsf.iteration.requireReview.help']}"> +

    diff --git a/server/zanata-war/src/main/webapp/WEB-INF/template/scripts.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/template/scripts.xhtml index 66d7e76a9d..0d4a0536d7 100644 --- a/server/zanata-war/src/main/webapp/WEB-INF/template/scripts.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/template/scripts.xhtml @@ -39,7 +39,7 @@ so that we can use jQuery(document).ready(). --> - + diff --git a/server/zanata-war/src/main/webapp/WEB-INF/template/template.xhtml b/server/zanata-war/src/main/webapp/WEB-INF/template/template.xhtml index cdaa021618..b2f25717cd 100755 --- a/server/zanata-war/src/main/webapp/WEB-INF/template/template.xhtml +++ b/server/zanata-war/src/main/webapp/WEB-INF/template/template.xhtml @@ -28,13 +28,14 @@ - - - + + - + - + + + + + + + + + + diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/google-logo.svg b/server/zanata-war/src/main/webapp/resources/assets/img/google-logo.svg new file mode 100644 index 0000000000..85dc345de7 --- /dev/null +++ b/server/zanata-war/src/main/webapp/resources/assets/img/google-logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo-text.svg b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo-text.svg new file mode 100644 index 0000000000..d37fbc38fe --- /dev/null +++ b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo-text.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.ico b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.ico new file mode 100644 index 0000000000..efe19e4349 Binary files /dev/null and b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.ico differ diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.png b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.png new file mode 100644 index 0000000000..fad5b9da92 Binary files /dev/null and b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.png differ diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.svg b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.svg new file mode 100644 index 0000000000..d6147082fe --- /dev/null +++ b/server/zanata-war/src/main/webapp/resources/assets/img/logo/logo.svg @@ -0,0 +1,13 @@ + + + + diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/openid-logo.svg b/server/zanata-war/src/main/webapp/resources/assets/img/openid-logo.svg new file mode 100644 index 0000000000..dda0ad9df6 --- /dev/null +++ b/server/zanata-war/src/main/webapp/resources/assets/img/openid-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/server/zanata-war/src/main/webapp/resources/assets/img/yahoo-logo.svg b/server/zanata-war/src/main/webapp/resources/assets/img/yahoo-logo.svg new file mode 100644 index 0000000000..ab0e0eb5e9 --- /dev/null +++ b/server/zanata-war/src/main/webapp/resources/assets/img/yahoo-logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/server/zanata-war/src/main/webapp/resources/assets/script.js b/server/zanata-war/src/main/webapp/resources/assets/script.js new file mode 100644 index 0000000000..65a05558d7 --- /dev/null +++ b/server/zanata-war/src/main/webapp/resources/assets/script.js @@ -0,0 +1,2581 @@ +/*! zanata-assets - v0.1.0 - 2017-02-02 +* http://zanata.org/ +* Copyright (c) 2017 Red Hat; Licensed MIT */ +;(function () { + 'use strict'; + + /** + * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. + * + * @codingstandard ftlabs-jsv2 + * @copyright The Financial Times Limited [All Rights Reserved] + * @license MIT License (see LICENSE.txt) + */ + + /*jslint browser:true, node:true*/ + /*global define, Event, Node*/ + + + /** + * Instantiate fast-clicking listeners on the specified layer. + * + * @constructor + * @param {Element} layer The layer to listen on + * @param {Object} [options={}] The options to override the defaults + */ + function FastClick(layer, options) { + var oldOnClick; + + options = options || {}; + + /** + * Whether a click is currently being tracked. + * + * @type boolean + */ + this.trackingClick = false; + + + /** + * Timestamp for when click tracking started. + * + * @type number + */ + this.trackingClickStart = 0; + + + /** + * The element being tracked for a click. + * + * @type EventTarget + */ + this.targetElement = null; + + + /** + * X-coordinate of touch start event. + * + * @type number + */ + this.touchStartX = 0; + + + /** + * Y-coordinate of touch start event. + * + * @type number + */ + this.touchStartY = 0; + + + /** + * ID of the last touch, retrieved from Touch.identifier. + * + * @type number + */ + this.lastTouchIdentifier = 0; + + + /** + * Touchmove boundary, beyond which a click will be cancelled. + * + * @type number + */ + this.touchBoundary = options.touchBoundary || 10; + + + /** + * The FastClick layer. + * + * @type Element + */ + this.layer = layer; + + /** + * The minimum time between tap(touchstart and touchend) events + * + * @type number + */ + this.tapDelay = options.tapDelay || 200; + + /** + * The maximum time for a tap + * + * @type number + */ + this.tapTimeout = options.tapTimeout || 700; + + if (FastClick.notNeeded(layer)) { + return; + } + + // Some old versions of Android don't have Function.prototype.bind + function bind(method, context) { + return function() { return method.apply(context, arguments); }; + } + + + var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']; + var context = this; + for (var i = 0, l = methods.length; i < l; i++) { + context[methods[i]] = bind(context[methods[i]], context); + } + + // Set up event handlers as required + if (deviceIsAndroid) { + layer.addEventListener('mouseover', this.onMouse, true); + layer.addEventListener('mousedown', this.onMouse, true); + layer.addEventListener('mouseup', this.onMouse, true); + } + + layer.addEventListener('click', this.onClick, true); + layer.addEventListener('touchstart', this.onTouchStart, false); + layer.addEventListener('touchmove', this.onTouchMove, false); + layer.addEventListener('touchend', this.onTouchEnd, false); + layer.addEventListener('touchcancel', this.onTouchCancel, false); + + // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick + // layer when they are cancelled. + if (!Event.prototype.stopImmediatePropagation) { + layer.removeEventListener = function(type, callback, capture) { + var rmv = Node.prototype.removeEventListener; + if (type === 'click') { + rmv.call(layer, type, callback.hijacked || callback, capture); + } else { + rmv.call(layer, type, callback, capture); + } + }; + + layer.addEventListener = function(type, callback, capture) { + var adv = Node.prototype.addEventListener; + if (type === 'click') { + adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { + if (!event.propagationStopped) { + callback(event); + } + }), capture); + } else { + adv.call(layer, type, callback, capture); + } + }; + } + + // If a handler is already declared in the element's onclick attribute, it will be fired before + // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and + // adding it as listener. + if (typeof layer.onclick === 'function') { + + // Android browser on at least 3.2 requires a new reference to the function in layer.onclick + // - the old one won't work if passed to addEventListener directly. + oldOnClick = layer.onclick; + layer.addEventListener('click', function(event) { + oldOnClick(event); + }, false); + layer.onclick = null; + } + } + + /** + * Windows Phone 8.1 fakes user agent string to look like Android and iPhone. + * + * @type boolean + */ + var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0; + + /** + * Android requires exceptions. + * + * @type boolean + */ + var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone; + + + /** + * iOS requires exceptions. + * + * @type boolean + */ + var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone; + + + /** + * iOS 4 requires an exception for select elements. + * + * @type boolean + */ + var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); + + + /** + * iOS 6.0-7.* requires the target element to be manually derived + * + * @type boolean + */ + var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent); + + /** + * BlackBerry requires exceptions. + * + * @type boolean + */ + var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0; + + /** + * Determine whether a given element requires a native click. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element needs a native click + */ + FastClick.prototype.needsClick = function(target) { + switch (target.nodeName.toLowerCase()) { + + // Don't send a synthetic click to disabled inputs (issue #62) + case 'button': + case 'select': + case 'textarea': + if (target.disabled) { + return true; + } + + break; + case 'input': + + // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) + if ((deviceIsIOS && target.type === 'file') || target.disabled) { + return true; + } + + break; + case 'label': + case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames + case 'video': + return true; + } + + return (/\bneedsclick\b/).test(target.className); + }; + + + /** + * Determine whether a given element requires a call to focus to simulate click into element. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. + */ + FastClick.prototype.needsFocus = function(target) { + switch (target.nodeName.toLowerCase()) { + case 'textarea': + return true; + case 'select': + return !deviceIsAndroid; + case 'input': + switch (target.type) { + case 'button': + case 'checkbox': + case 'file': + case 'image': + case 'radio': + case 'submit': + return false; + } + + // No point in attempting to focus disabled inputs + return !target.disabled && !target.readOnly; + default: + return (/\bneedsfocus\b/).test(target.className); + } + }; + + + /** + * Send a click event to the specified element. + * + * @param {EventTarget|Element} targetElement + * @param {Event} event + */ + FastClick.prototype.sendClick = function(targetElement, event) { + var clickEvent, touch; + + // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) + if (document.activeElement && document.activeElement !== targetElement) { + document.activeElement.blur(); + } + + touch = event.changedTouches[0]; + + // Synthesise a click event, with an extra attribute so it can be tracked + clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + clickEvent.forwardedTouchEvent = true; + targetElement.dispatchEvent(clickEvent); + }; + + FastClick.prototype.determineEventType = function(targetElement) { + + //Issue #159: Android Chrome Select Box does not open with a synthetic click event + if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { + return 'mousedown'; + } + + return 'click'; + }; + + + /** + * @param {EventTarget|Element} targetElement + */ + FastClick.prototype.focus = function(targetElement) { + var length; + + // Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. + if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') { + length = targetElement.value.length; + targetElement.setSelectionRange(length, length); + } else { + targetElement.focus(); + } + }; + + + /** + * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. + * + * @param {EventTarget|Element} targetElement + */ + FastClick.prototype.updateScrollParent = function(targetElement) { + var scrollParent, parentElement; + + scrollParent = targetElement.fastClickScrollParent; + + // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the + // target element was moved to another parent. + if (!scrollParent || !scrollParent.contains(targetElement)) { + parentElement = targetElement; + do { + if (parentElement.scrollHeight > parentElement.offsetHeight) { + scrollParent = parentElement; + targetElement.fastClickScrollParent = parentElement; + break; + } + + parentElement = parentElement.parentElement; + } while (parentElement); + } + + // Always update the scroll top tracker if possible. + if (scrollParent) { + scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; + } + }; + + + /** + * @param {EventTarget} targetElement + * @returns {Element|EventTarget} + */ + FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { + + // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. + if (eventTarget.nodeType === Node.TEXT_NODE) { + return eventTarget.parentNode; + } + + return eventTarget; + }; + + + /** + * On touch start, record the position and scroll offset. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onTouchStart = function(event) { + var targetElement, touch, selection; + + // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). + if (event.targetTouches.length > 1) { + return true; + } + + targetElement = this.getTargetElementFromEventTarget(event.target); + touch = event.targetTouches[0]; + + if (deviceIsIOS) { + + // Only trusted events will deselect text on iOS (issue #49) + selection = window.getSelection(); + if (selection.rangeCount && !selection.isCollapsed) { + return true; + } + + if (!deviceIsIOS4) { + + // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): + // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched + // with the same identifier as the touch event that previously triggered the click that triggered the alert. + // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an + // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. + // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string, + // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long, + // random integers, it's safe to to continue if the identifier is 0 here. + if (touch.identifier && touch.identifier === this.lastTouchIdentifier) { + event.preventDefault(); + return false; + } + + this.lastTouchIdentifier = touch.identifier; + + // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: + // 1) the user does a fling scroll on the scrollable layer + // 2) the user stops the fling scroll with another tap + // then the event.target of the last 'touchend' event will be the element that was under the user's finger + // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check + // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). + this.updateScrollParent(targetElement); + } + } + + this.trackingClick = true; + this.trackingClickStart = event.timeStamp; + this.targetElement = targetElement; + + this.touchStartX = touch.pageX; + this.touchStartY = touch.pageY; + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { + event.preventDefault(); + } + + return true; + }; + + + /** + * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.touchHasMoved = function(event) { + var touch = event.changedTouches[0], boundary = this.touchBoundary; + + if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { + return true; + } + + return false; + }; + + + /** + * Update the last position. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onTouchMove = function(event) { + if (!this.trackingClick) { + return true; + } + + // If the touch has moved, cancel the click tracking + if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { + this.trackingClick = false; + this.targetElement = null; + } + + return true; + }; + + + /** + * Attempt to find the labelled control for the given label element. + * + * @param {EventTarget|HTMLLabelElement} labelElement + * @returns {Element|null} + */ + FastClick.prototype.findControl = function(labelElement) { + + // Fast path for newer browsers supporting the HTML5 control attribute + if (labelElement.control !== undefined) { + return labelElement.control; + } + + // All browsers under test that support touch events also support the HTML5 htmlFor attribute + if (labelElement.htmlFor) { + return document.getElementById(labelElement.htmlFor); + } + + // If no for attribute exists, attempt to retrieve the first labellable descendant element + // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label + return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); + }; + + + /** + * On touch end, determine whether to send a click event at once. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onTouchEnd = function(event) { + var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; + + if (!this.trackingClick) { + return true; + } + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { + this.cancelNextClick = true; + return true; + } + + if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { + return true; + } + + // Reset to prevent wrong click cancel on input (issue #156). + this.cancelNextClick = false; + + this.lastClickTime = event.timeStamp; + + trackingClickStart = this.trackingClickStart; + this.trackingClick = false; + this.trackingClickStart = 0; + + // On some iOS devices, the targetElement supplied with the event is invalid if the layer + // is performing a transition or scroll, and has to be re-detected manually. Note that + // for this to function correctly, it must be called *after* the event target is checked! + // See issue #57; also filed as rdar://13048589 . + if (deviceIsIOSWithBadTarget) { + touch = event.changedTouches[0]; + + // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null + targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; + targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; + } + + targetTagName = targetElement.tagName.toLowerCase(); + if (targetTagName === 'label') { + forElement = this.findControl(targetElement); + if (forElement) { + this.focus(targetElement); + if (deviceIsAndroid) { + return false; + } + + targetElement = forElement; + } + } else if (this.needsFocus(targetElement)) { + + // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. + // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). + if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { + this.targetElement = null; + return false; + } + + this.focus(targetElement); + this.sendClick(targetElement, event); + + // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. + // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) + if (!deviceIsIOS || targetTagName !== 'select') { + this.targetElement = null; + event.preventDefault(); + } + + return false; + } + + if (deviceIsIOS && !deviceIsIOS4) { + + // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled + // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). + scrollParent = targetElement.fastClickScrollParent; + if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { + return true; + } + } + + // Prevent the actual click from going though - unless the target node is marked as requiring + // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. + if (!this.needsClick(targetElement)) { + event.preventDefault(); + this.sendClick(targetElement, event); + } + + return false; + }; + + + /** + * On touch cancel, stop tracking the click. + * + * @returns {void} + */ + FastClick.prototype.onTouchCancel = function() { + this.trackingClick = false; + this.targetElement = null; + }; + + + /** + * Determine mouse events which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onMouse = function(event) { + + // If a target element was never set (because a touch event was never fired) allow the event + if (!this.targetElement) { + return true; + } + + if (event.forwardedTouchEvent) { + return true; + } + + // Programmatically generated events targeting a specific element should be permitted + if (!event.cancelable) { + return true; + } + + // Derive and check the target element to see whether the mouse event needs to be permitted; + // unless explicitly enabled, prevent non-touch click events from triggering actions, + // to prevent ghost/doubleclicks. + if (!this.needsClick(this.targetElement) || this.cancelNextClick) { + + // Prevent any user-added listeners declared on FastClick element from being fired. + if (event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } else { + + // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + event.propagationStopped = true; + } + + // Cancel the event + event.stopPropagation(); + event.preventDefault(); + + return false; + } + + // If the mouse event is permitted, return true for the action to go through. + return true; + }; + + + /** + * On actual clicks, determine whether this is a touch-generated click, a click action occurring + * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or + * an actual click which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onClick = function(event) { + var permitted; + + // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. + if (this.trackingClick) { + this.targetElement = null; + this.trackingClick = false; + return true; + } + + // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. + if (event.target.type === 'submit' && event.detail === 0) { + return true; + } + + permitted = this.onMouse(event); + + // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. + if (!permitted) { + this.targetElement = null; + } + + // If clicks are permitted, return true for the action to go through. + return permitted; + }; + + + /** + * Remove all FastClick's event listeners. + * + * @returns {void} + */ + FastClick.prototype.destroy = function() { + var layer = this.layer; + + if (deviceIsAndroid) { + layer.removeEventListener('mouseover', this.onMouse, true); + layer.removeEventListener('mousedown', this.onMouse, true); + layer.removeEventListener('mouseup', this.onMouse, true); + } + + layer.removeEventListener('click', this.onClick, true); + layer.removeEventListener('touchstart', this.onTouchStart, false); + layer.removeEventListener('touchmove', this.onTouchMove, false); + layer.removeEventListener('touchend', this.onTouchEnd, false); + layer.removeEventListener('touchcancel', this.onTouchCancel, false); + }; + + + /** + * Check whether FastClick is needed. + * + * @param {Element} layer The layer to listen on + */ + FastClick.notNeeded = function(layer) { + var metaViewport; + var chromeVersion; + var blackberryVersion; + var firefoxVersion; + + // Devices that don't support touch don't need FastClick + if (typeof window.ontouchstart === 'undefined') { + return true; + } + + // Chrome version - zero for other browsers + chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; + + if (chromeVersion) { + + if (deviceIsAndroid) { + metaViewport = document.querySelector('meta[name=viewport]'); + + if (metaViewport) { + // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) + if (metaViewport.content.indexOf('user-scalable=no') !== -1) { + return true; + } + // Chrome 32 and above with width=device-width or less don't need FastClick + if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) { + return true; + } + } + + // Chrome desktop doesn't need FastClick (issue #15) + } else { + return true; + } + } + + if (deviceIsBlackBerry10) { + blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/); + + // BlackBerry 10.3+ does not require Fastclick library. + // https://github.com/ftlabs/fastclick/issues/251 + if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) { + metaViewport = document.querySelector('meta[name=viewport]'); + + if (metaViewport) { + // user-scalable=no eliminates click delay. + if (metaViewport.content.indexOf('user-scalable=no') !== -1) { + return true; + } + // width=device-width (or less than device-width) eliminates click delay. + if (document.documentElement.scrollWidth <= window.outerWidth) { + return true; + } + } + } + } + + // IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97) + if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') { + return true; + } + + // Firefox version - zero for other browsers + firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; + + if (firefoxVersion >= 27) { + // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896 + + metaViewport = document.querySelector('meta[name=viewport]'); + if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) { + return true; + } + } + + // IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version + // http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx + if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') { + return true; + } + + return false; + }; + + + /** + * Factory method for creating a FastClick object + * + * @param {Element} layer The layer to listen on + * @param {Object} [options={}] The options to override the defaults + */ + FastClick.attach = function(layer, options) { + return new FastClick(layer, options); + }; + + + if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { + + // AMD. Register as an anonymous module. + define(function() { + return FastClick; + }); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = FastClick.attach; + module.exports.FastClick = FastClick; + } else { + window.FastClick = FastClick; + } +}()); + +; + + + +window.Modernizr = (function( window, document, undefined ) { + + var version = '2.6.2', + + Modernizr = {}, + + enableClasses = true, + + docElement = document.documentElement, + + mod = 'modernizr', + modElem = document.createElement(mod), + mStyle = modElem.style, + + inputElem , + + + toString = {}.toString, + + prefixes = ' -webkit- -moz- -o- -ms- '.split(' '), + + + + omPrefixes = 'Webkit Moz O ms', + + cssomPrefixes = omPrefixes.split(' '), + + domPrefixes = omPrefixes.toLowerCase().split(' '), + + ns = {'svg': 'http://www.w3.org/2000/svg'}, + + tests = {}, + inputs = {}, + attrs = {}, + + classes = [], + + slice = classes.slice, + + featureName, + + + injectElementWithStyles = function( rule, callback, nodes, testnames ) { + + var style, ret, node, docOverflow, + div = document.createElement('div'), + body = document.body, + fakeBody = body || document.createElement('body'); + + if ( parseInt(nodes, 10) ) { + while ( nodes-- ) { + node = document.createElement('div'); + node.id = testnames ? testnames[nodes] : mod + (nodes + 1); + div.appendChild(node); + } + } + + style = ['­',''].join(''); + div.id = mod; + (body ? div : fakeBody).innerHTML += style; + fakeBody.appendChild(div); + if ( !body ) { + fakeBody.style.background = ''; + fakeBody.style.overflow = 'hidden'; + docOverflow = docElement.style.overflow; + docElement.style.overflow = 'hidden'; + docElement.appendChild(fakeBody); + } + + ret = callback(div, rule); + if ( !body ) { + fakeBody.parentNode.removeChild(fakeBody); + docElement.style.overflow = docOverflow; + } else { + div.parentNode.removeChild(div); + } + + return !!ret; + + }, + _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp; + + if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) { + hasOwnProp = function (object, property) { + return _hasOwnProperty.call(object, property); + }; + } + else { + hasOwnProp = function (object, property) { + return ((property in object) && is(object.constructor.prototype[property], 'undefined')); + }; + } + + + if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { + + var target = this; + + if (typeof target != "function") { + throw new TypeError(); + } + + var args = slice.call(arguments, 1), + bound = function () { + + if (this instanceof bound) { + + var F = function(){}; + F.prototype = target.prototype; + var self = new F(); + + var result = target.apply( + self, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return self; + + } else { + + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + + return bound; + }; + } + + function setCss( str ) { + mStyle.cssText = str; + } + + function setCssAll( str1, str2 ) { + return setCss(prefixes.join(str1 + ';') + ( str2 || '' )); + } + + function is( obj, type ) { + return typeof obj === type; + } + + function contains( str, substr ) { + return !!~('' + str).indexOf(substr); + } + + function testProps( props, prefixed ) { + for ( var i in props ) { + var prop = props[i]; + if ( !contains(prop, "-") && mStyle[prop] !== undefined ) { + return prefixed == 'pfx' ? prop : true; + } + } + return false; + } + + function testDOMProps( props, obj, elem ) { + for ( var i in props ) { + var item = obj[props[i]]; + if ( item !== undefined) { + + if (elem === false) return props[i]; + + if (is(item, 'function')){ + return item.bind(elem || obj); + } + + return item; + } + } + return false; + } + + function testPropsAll( prop, prefixed, elem ) { + + var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), + props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' '); + + if(is(prefixed, "string") || is(prefixed, "undefined")) { + return testProps(props, prefixed); + + } else { + props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' '); + return testDOMProps(props, prefixed, elem); + } + } tests['touch'] = function() { + var bool; + + if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { + bool = true; + } else { + injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) { + bool = node.offsetTop === 9; + }); + } + + return bool; + }; + tests['csstransforms3d'] = function() { + + var ret = !!testPropsAll('perspective'); + + if ( ret && 'webkitPerspective' in docElement.style ) { + + injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) { + ret = node.offsetLeft === 9 && node.offsetHeight === 3; + }); + } + return ret; + }; + tests['fontface'] = function() { + var bool; + + injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) { + var style = document.getElementById('smodernizr'), + sheet = style.sheet || style.styleSheet, + cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : ''; + + bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; + }); + + return bool; + }; + + tests['svg'] = function() { + return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect; + }; + for ( var feature in tests ) { + if ( hasOwnProp(tests, feature) ) { + featureName = feature.toLowerCase(); + Modernizr[featureName] = tests[feature](); + + classes.push((Modernizr[featureName] ? '' : 'no-') + featureName); + } + } + + + + Modernizr.addTest = function ( feature, test ) { + if ( typeof feature == 'object' ) { + for ( var key in feature ) { + if ( hasOwnProp( feature, key ) ) { + Modernizr.addTest( key, feature[ key ] ); + } + } + } else { + + feature = feature.toLowerCase(); + + if ( Modernizr[feature] !== undefined ) { + return Modernizr; + } + + test = typeof test == 'function' ? test() : test; + + if (typeof enableClasses !== "undefined" && enableClasses) { + docElement.className += ' ' + (test ? '' : 'no-') + feature; + } + Modernizr[feature] = test; + + } + + return Modernizr; + }; + + + setCss(''); + modElem = inputElem = null; + + ;(function(window, document) { + var options = window.html5 || {}; + + var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i; + + var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i; + + var supportsHtml5Styles; + + var expando = '_html5shiv'; + + var expanID = 0; + + var expandoData = {}; + + var supportsUnknownElements; + + (function() { + try { + var a = document.createElement('a'); + a.innerHTML = ''; + supportsHtml5Styles = ('hidden' in a); + + supportsUnknownElements = a.childNodes.length == 1 || (function() { + (document.createElement)('a'); + var frag = document.createDocumentFragment(); + return ( + typeof frag.cloneNode == 'undefined' || + typeof frag.createDocumentFragment == 'undefined' || + typeof frag.createElement == 'undefined' + ); + }()); + } catch(e) { + supportsHtml5Styles = true; + supportsUnknownElements = true; + } + + }()); function addStyleSheet(ownerDocument, cssText) { + var p = ownerDocument.createElement('p'), + parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement; + + p.innerHTML = 'x'; + return parent.insertBefore(p.lastChild, parent.firstChild); + } + + function getElements() { + var elements = html5.elements; + return typeof elements == 'string' ? elements.split(' ') : elements; + } + + function getExpandoData(ownerDocument) { + var data = expandoData[ownerDocument[expando]]; + if (!data) { + data = {}; + expanID++; + ownerDocument[expando] = expanID; + expandoData[expanID] = data; + } + return data; + } + + function createElement(nodeName, ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createElement(nodeName); + } + if (!data) { + data = getExpandoData(ownerDocument); + } + var node; + + if (data.cache[nodeName]) { + node = data.cache[nodeName].cloneNode(); + } else if (saveClones.test(nodeName)) { + node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode(); + } else { + node = data.createElem(nodeName); + } + + return node.canHaveChildren && !reSkip.test(nodeName) ? data.frag.appendChild(node) : node; + } + + function createDocumentFragment(ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createDocumentFragment(); + } + data = data || getExpandoData(ownerDocument); + var clone = data.frag.cloneNode(), + i = 0, + elems = getElements(), + l = elems.length; + for(;i> 1) : parseInt(o.left, 10) + mid) + 'px', + top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px' + }) + } + + el.setAttribute('role', 'progressbar') + self.lines(el, self.opts) + + if (!useCssAnimations) { + // No CSS animation support, use setTimeout() instead + var i = 0 + , start = (o.lines - 1) * (1 - o.direction) / 2 + , alpha + , fps = o.fps + , f = fps/o.speed + , ostep = (1-o.opacity) / (f*o.trail / 100) + , astep = f/o.lines + + ;(function anim() { + i++; + for (var j = 0; j < o.lines; j++) { + alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity) + + self.opacity(el, j * o.direction + start, alpha, o) + } + self.timeout = self.el && setTimeout(anim, ~~(1000/fps)) + })() + } + return self + }, + + /** + * Stops and removes the Spinner. + */ + stop: function() { + var el = this.el + if (el) { + clearTimeout(this.timeout) + if (el.parentNode) el.parentNode.removeChild(el) + this.el = undefined + } + return this + }, + + /** + * Internal method that draws the individual lines. Will be overwritten + * in VML fallback mode below. + */ + lines: function(el, o) { + var i = 0 + , start = (o.lines - 1) * (1 - o.direction) / 2 + , seg + + function fill(color, shadow) { + return css(createEl(), { + position: 'absolute', + width: (o.length+o.width) + 'px', + height: o.width + 'px', + background: color, + boxShadow: shadow, + transformOrigin: 'left', + transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)', + borderRadius: (o.corners * o.width>>1) + 'px' + }) + } + + for (; i < o.lines; i++) { + seg = css(createEl(), { + position: 'absolute', + top: 1+~(o.width/2) + 'px', + transform: o.hwaccel ? 'translate3d(0,0,0)' : '', + opacity: o.opacity, + animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1/o.speed + 's linear infinite' + }) + + if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'})) + + ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)'))) + } + return el + }, + + /** + * Internal method that adjusts the opacity of a single line. + * Will be overwritten in VML fallback mode below. + */ + opacity: function(el, i, val) { + if (i < el.childNodes.length) el.childNodes[i].style.opacity = val + } + + }) + + + function initVML() { + + /* Utility function to create a VML tag */ + function vml(tag, attr) { + return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr) + } + + // No CSS transforms but VML support, add a CSS rule for VML elements: + sheet.addRule('.spin-vml', 'behavior:url(#default#VML)') + + Spinner.prototype.lines = function(el, o) { + var r = o.length+o.width + , s = 2*r + + function grp() { + return css( + vml('group', { + coordsize: s + ' ' + s, + coordorigin: -r + ' ' + -r + }), + { width: s, height: s } + ) + } + + var margin = -(o.width+o.length)*2 + 'px' + , g = css(grp(), {position: 'absolute', top: margin, left: margin}) + , i + + function seg(i, dx, filter) { + ins(g, + ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}), + ins(css(vml('roundrect', {arcsize: o.corners}), { + width: r, + height: o.width, + left: o.radius, + top: -o.width>>1, + filter: filter + }), + vml('fill', {color: o.color, opacity: o.opacity}), + vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change + ) + ) + ) + } + + if (o.shadow) + for (i = 1; i <= o.lines; i++) + seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)') + + for (i = 1; i <= o.lines; i++) seg(i) + return ins(el, g) + } + + Spinner.prototype.opacity = function(el, i, val, o) { + var c = el.firstChild + o = o.shadow && o.lines || 0 + if (c && i+o < c.childNodes.length) { + c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild + if (c) c.opacity = val + } + } + } + + var probe = css(createEl('group'), {behavior: 'url(#default#VML)'}) + + if (!vendor(probe, 'transform') && probe.adj) initVML() + else useCssAnimations = vendor(probe, 'animation') + + return Spinner + +})); + +/*! + * Bootstrap v3.0.3 + * + * Copyright 2013 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + ++function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);var e=this.options.trigger.split(" ");for(var f=e.length;f--;){var g=e[f];if(g=="click")this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if(g!="manual"){var h=g=="hover"?"mouseenter":"focus",i=g=="hover"?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&typeof b.delay=="number"&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);clearTimeout(c.timeout),c.hoverState="in";if(!c.options.delay||!c.options.delay.show)return c.show();c.timeout=setTimeout(function(){c.hoverState=="in"&&c.show()},c.options.delay.show)},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);clearTimeout(c.timeout),c.hoverState="out";if(!c.options.delay||!c.options.delay.hide)return c.hide();c.timeout=setTimeout(function(){c.hoverState=="out"&&c.hide()},c.options.delay.hide)},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);if(b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d=typeof this.options.placement=="function"?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m=this.options.container=="body"?window.innerWidth:j.outerWidth(),n=this.options.container=="body"?window.innerHeight:j.outerHeight(),o=this.options.container=="body"?0:j.offset().left;d=d=="bottom"&&g.top+g.height+i-l>n?"top":d=="top"&&g.top-l-i<0?"bottom":d=="right"&&g.right+h>m?"left":d=="left"&&g.left-h 0 || + $target.hasClass('js-form--search'); + + if (!formSearchInProgress && !formSearchInputMouseDown) { + $('.js-form--search__input').blur(); + } + } + + var appendCheckboxes = function (el, callback) { + + var $elCheckboxes; + + el = el || 'body'; + $elCheckboxes = $(el).find('.js-form__checkbox'); + + $.each($elCheckboxes, function () { + var $this = $(this); + + if (!$this.find('.form__checkbox__item').length) { + $this + .append(''); + setCheckRadioStatus($this); + } + + }); + + if (typeof callback === 'function') { + callback(); + } + + }; + + var appendRadios = function (el, callback) { + + var $elRadios; + + el = el || 'body', + $elRadios = $(el).find('.js-form__radio'); + + $.each($elRadios, function () { + var $this = $(this); + + if (!$this.find('.form__radio__item').length) { + $this + .append(''); + setCheckRadioStatus($this); + } + + }); + + if (typeof callback === 'function') { + callback(); + } + + }; + + var enableInputLoading = function(el, callback) { + var $el = el ? $(el).find('.js-form__input--load') : + $('.js-form__input--load'), + $elParent = $(el).parent(), + $loader = $('') + .addClass('js-loader form__loader loader loader--mini'); + + // Add a loader if there isn't one + if (!$elParent.find('.js-loader').length) { + $el.addClass('form__input--load'); + $elParent.addClass('js-form__load form__load').append($loader); + } + + if (typeof callback === 'function') { + callback(); + } + }; + + var activateInputLoading = function(el) { + enableInputLoading(el, function() { + var $elParent = $(el).parent(), + $loader = $elParent.find('.js-loader'); + + zanata.loader.activate($loader); + $elParent.addClass('is-loading'); + }); + }; + + var deactivateInputLoading = function(el) { + enableInputLoading(el, function() { + var $elParent = $(el).parent(), + $loader = $elParent.find('.js-loader'); + + zanata.loader.deactivate($loader); + $elParent.removeClass('is-loading'); + }); + }; + + + // Form Clear + + var clearFormInit = function() { + + $('.js-form__input--clear').addClass('form__input--clear') + .parent().addClass('form__clear js-form__clear') + .append(''); + + clearFormBindings(); + }; + + var clearFormBindings = function() { + + $('.js-form__button--clear').on('click', function (e) { + e.preventDefault(); + $(this).prev('.js-form__input--clear').val('').focus(); + $(this).addClass('is-hidden'); + }); + + $('.js-form__input--clear').on('keyup', function () { + var $this = $(this), + val = $this.val(), + $clearButton = $this.next('.js-form__button--clear'); + + if (val !== '') { + $clearButton.removeClass('is-hidden'); + } else { + $clearButton.addClass('is-hidden'); + } + }); + + }; + + var radioBindings = function(el) { + el = el || 'body'; + $(el).on('click', '.js-form__radio', function (e) { + setCheckRadio($(this)); + e.preventDefault(); + }); + + $(el).on('change', '.js-form__radio__input', function (e) { + var $parent = $(this).parents('.js-form__radio'); + removeRadioStatus($parent); + setCheckRadioStatus($parent); + }); + + $(el).on('disable', '.js-form__radio__input', function (e) { + var $parent = $(this).parents('.js-form__radio'); + toggleDisableCheckRadio($parent, true); + }); + + $(el).on('enable', '.js-form__radio__input', function (e) { + var $parent = $(this).parents('.js-form__radio'); + toggleDisableCheckRadio($parent, false); + }); + }; + + var checkboxBindings = function(el) { + el = el || 'body'; + $(el).on('click', '.js-form__checkbox', function (e) { + + if ($(this).hasClass('is-disabled')) { + return false; + } + + var directClick = e.target === e.currentTarget; + var tagName = e.target.tagName.toLowerCase(); + var clickOnButton = tagName === 'button'; + var clickOnAnchor = tagName === 'a'; + var clickOnTextbox = tagName === 'input' && e.target.type === 'text'; + var clickOnSubmit = tagName === 'input' && e.target.type === 'submit'; + + var clickOnClickHandler = clickOnButton || clickOnAnchor || + clickOnTextbox || clickOnSubmit; + + if (directClick || !clickOnClickHandler) { + e.preventDefault(); + setCheckRadio($(this)); + } + }); + + $(el).on('change', '.js-form__checkbox__input', function (e) { + var $parent = $(this).parents('.js-form__checkbox'); + setCheckRadioStatus($parent); + }); + + $(el).on('disable', '.js-form__checkbox__input', function (e) { + var $parent = $(this).parents('.js-form__checkbox'); + toggleDisableCheckRadio($parent, true); + }); + + $(el).on('enable', '.js-form__checkbox__input', function (e) { + var $parent = $(this).parents('.js-form__checkbox'); + toggleDisableCheckRadio($parent, false); + }); + + }; + + var init = function (el) { + + el = el || 'body'; + + appendCheckboxes(el, checkboxBindings(el)); + appendRadios(el, radioBindings(el)); + enableInputLoading(); + clearFormInit(el); + + $('.js-form-password-parent') + .on('click', '.js-form-password-toggle', function (e) { + + var $passwordInput = $(this) + .parents('.js-form-password-parent') + .find('.js-form-password-input'); + + e.preventDefault(); + + if ($passwordInput.attr('type') === 'password') { + $passwordInput.attr({ + 'type': 'text', + 'autocapitalize': 'off', + 'autocomplete': 'off', + 'autocorrect': 'off', + 'spellcheck': 'false' + }); + $(this).text('Hide'); + } + else { + $passwordInput.attr('type', 'password'); + $(this).text('Show'); + } + + $passwordInput.focus(); + }); + + $('.js-form--search__input, .js-form--search__button').on('click', + function (e) { + e.stopPropagation(); + } + ); + + $('.js-form--search__input, .js-form--search__button').on('focus', + function () { + $(this).parents('.js-form--search').addClass('is-active'); + } + ); + + $('.js-form--search__input, .js-form--search__button').on('blur', + function (e) { + if (!formSearchInProgress) { + $(this).parents('.js-form--search').removeClass('is-active'); + } + } + ); + + $('.js-form--search').on('mousedown', function(e) { + formSearchInputMouseDown = + $(e.target).hasClass('js-form--search__input'); + updateSearchProgressFlag(e); + }); + + $(document).on('mouseup', function(e) { + updateSearchProgressFlag(e); + // Reset mouse down + formSearchInputMouseDown = false; + }); + + $('.js-form__input--copyable') + .on('mouseup', function () { + var $this = $(this), + thisItem = $this[0]; + if (thisItem.selectionStart === thisItem.selectionEnd) { + $this.select(); + } + }); + + }; + + // public API + return { + init: init, + appendCheckboxes: appendCheckboxes, + appendRadios: appendRadios, + checkboxBindings: checkboxBindings, + radioBindings: radioBindings, + activateInputLoading: activateInputLoading, + deactivateInputLoading: deactivateInputLoading + }; + +})(jQuery); + +jQuery(function () { + zanata.form.init(); +}); + +'use strict'; + +zanata.createNS('zanata.loader'); + +zanata.loader = (function ($) { + + var activate = function (el) { + var $el = $(el), + $label = $el.find('.loader__label'); + + if ($label.length > 0) { + $label.append('' + + ''); + } + else { + $el.append('' + + ''); + } + + $el.addClass('is-active'); + }; + + var deactivate = function (el) { + var $el = $(el); + + $el.find('.loader__spinner').remove(); + $el.removeClass('is-active'); + }; + + var init = function () { + + $(document).on('click', '.js-loader, .loader', function (e) { + // If it is not active + e.preventDefault(); + if (!$(this).hasClass('is-active')) { + activate(this); + } + }); + + }; + + // public API + return { + init: init, + activate: activate, + deactivate: deactivate + }; + +})(jQuery); + +jQuery(function () { + zanata.loader.init(); +}); + + +'use strict'; + +zanata.createNS('zanata.messages'); + +zanata.messages = (function ($) { + + var hide = function (el, e) { + var $el = $(el); + if (e) e.preventDefault(); + if ($el.hasClass('is-active')) { + $el.removeClass('is-active'); + setTimeout(function () { + $el.remove(); + }, 300); + } + else { + $el.addClass('is-removed'); + setTimeout(function () { + $el.remove(); + }, 300); + } + }; + + var activate = function (el) { + $(el).addClass('is-active'); + updatePosition(el); + }; + + var deactivate = function (el) { + $(el).removeClass('is-active'); + }; + + var updatePosition = function (el, elPositionFromTop) { + var $el = $(el), + elPosFromTop = ''; + + if (typeof elPositionFromTop !== 'undefined') { + elPosFromTop = elPositionFromTop; + } + else if($el.length > 0) { + elPosFromTop = $el.offset().top; + } + else { + return; + } + + // Stop negative values setting the position to fixed + if (elPosFromTop < 0) elPosFromTop = 0; + + if ($(window).scrollTop() > elPosFromTop) { + $el.addClass('is-fixed'); + } else { + $el.removeClass('is-fixed'); + } + }; + + var init = function () { + + if ($('.message--global').length > 0) { + var messageGlobalTop = $('.message--global').offset().top; + } + + $(document).on('click', '.js-message-remove', function (e) { + var $el = $(this).parents('.message--removable'); + hide($el, e); + }); + + $(window).scroll(function (){ + updatePosition('.message--global', messageGlobalTop); + }); + }; + + // public API + return { + init: init, + hide: hide, + activate: activate, + deactivate: deactivate, + updatePosition: updatePosition + }; + +})(jQuery); + +jQuery(function () { + zanata.messages.init(); +}); + +'use strict'; + +zanata.createNS('zanata.modal'); + +zanata.modal = (function ($) { + + var show = function (el) { + var $el = $(el); + $el.addClass('is-active').scrollTop(0); + $('body').addClass('is-modal').css('padding-right', getScrollBarWidth()); + }; + + var hide = function (el) { + var $el = $(el); + $el.removeClass('is-active'); + $('body').removeClass('is-modal').removeAttr('style'); + }; + + var init = function () { + + $(document).on('click', '[data-toggle="modal"]', function () { + var modalTarget = $(this).attr('data-target'); + $(modalTarget).trigger('show.zanata.modal'); + }); + + $(document).on('click', '.is-modal', function (e) { + if ($(e.target).not('.modal__dialog') && + !$(e.target).parents('.modal__dialog').length) { + $('.modal.is-active').trigger('hide.zanata.modal'); + } + }); + + $(document).on('keyup', function (e) { + if (e.keyCode === 27) { + e.stopPropagation(); + $('.modal.is-active').trigger('hide.zanata.modal'); + } + }); + + $(document).on('click', '[data-dismiss="modal"]', function () { + $(this).parents('.modal.is-active').trigger('hide.zanata.modal'); + }); + + $(document).on('hide.zanata.modal', function (e) { + hide(e.target); + }); + + $(document).on('show.zanata.modal', function (e) { + show(e.target); + }); + + }; + + function getScrollBarWidth () { + var $outer = $('
    ').css({visibility: 'hidden', width: 100, + overflow: 'scroll'}).appendTo('body'), + widthWithScroll = $('
    ').css({width: '100%'}) + .appendTo($outer).outerWidth(); + $outer.remove(); + return 100 - widthWithScroll; + } + + // public API + return { + init: init, + show: show, + hide: hide + }; + +})(jQuery); + +jQuery(function () { + zanata.modal.init(); +}); + +'use strict'; + +zanata.createNS('zanata.panel'); + +zanata.panel = (function ($) { + + var init = function () { + var $panelBody = $('.js-panel__body'); + var resizeTimeout; + function resizePanels() { + var windowHeight = $(window).height(); + $.each($panelBody, function(i) { + var $this = $(this); + var $panel = $this.parents('.js-panel'); + var panelFromTop = $panelBody[i].getBoundingClientRect().top; + var footerHeight = $('.js-footer').height(); + var panelHeight = Math.floor( + // Minus 2 to account for rounding errors + windowHeight - panelFromTop - footerHeight - 2 + ); + $this.css('height', panelHeight); + }); + } + if ($panelBody.length > 0) { + $(window).resize(function(event) { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(resizePanels, 0); + }); + resizePanels(); + } + }; + + // public API + return { + init: init + }; + +})(jQuery); + +jQuery(function () { + zanata.panel.init(); +}); + +(function ($) { + 'use strict'; + $(document).on('click', '.js-reveal__show', function () { + var $revealTarget = $($(this).attr('data-target')), + $revealTargetInput = $revealTarget.find('.js-reveal__target__input'), + $revealParent = $(this).parents('.js-reveal'); + $(this).addClass('is-hidden'); + $revealParent.addClass('is-active'); + $revealTarget.toggleClass('is-active'); + setTimeout(function () { + $revealTargetInput.focus(); + }, 100); + }); + $(document).on('click', '.js-reveal__toggle', function (e) { + var $revealTarget = $($(this).attr('data-target')), + $revealTargetInput = $revealTarget.find('.js-reveal__target__input'), + $revealParent = $(this).parents('.js-reveal'), + $revealText = $(this).find('.js-reveal__toggle__text'), + revealTextValue = $revealText.text(), + revealToggleValue = $revealText.attr('data-toggle-value'), + revealTitle = $(this).attr('title') || + $(this).attr('data-original-title'), + revealToggleTitle = $(this).attr('data-toggle-title'); + + // Label need to register the click so it applies to the checkbox or radio + // it is attached to + if (!$(e.target).is('label')) { + e.preventDefault(); + } + $(this).toggleClass('is-active'); + $revealParent.toggleClass('is-active'); + $revealTarget.toggleClass('is-active is-hidden'); + if (revealToggleTitle && revealTitle) { + $(this).attr('data-toggle-title', revealTitle); + zanata.tooltip.refresh($(this), revealToggleTitle); + } + if (revealTextValue && revealToggleValue) { + $revealText.text(revealToggleValue); + $revealText.attr('data-toggle-value', revealTextValue); + } + setTimeout(function () { + $revealTargetInput.focus(); + }, 100); + }); + + $(document).on('click', '.js-reveal__reset', function () { + var $revealTarget = $($(this).attr('data-target')), + $revealTargetInput = $revealTarget.find('.js-reveal__target__input'); + $revealTargetInput.val('').focus(); + $(this).addClass('is-hidden'); + }); + $(document).on('click', '.js-reveal__cancel', function () { + var $revealTarget = $($(this).attr('data-target')), + $revealTargetInput = $revealTarget.find('.js-reveal__target__input'), + $revealParent = $(this).parents('.js-reveal'); + $revealTarget.removeClass('is-active'); + $revealTargetInput.blur(); + $revealTargetInput.val(''); + $revealParent.find('.js-reveal__reset').addClass('is-hidden'); + $revealParent.find('.js-reveal__show').removeClass('is-hidden').focus(); + }); + $(document).on('keyup', '.js-reveal__target__input', function (e) { + var $revealParent = $(this).parents('.js-reveal'), + $revealReset = $revealParent.find('.js-reveal__reset'), + $revealCancel = $revealParent.find('.js-reveal__cancel'); + if ($(this).val() !== '') { + $revealReset.removeClass('is-hidden'); + } + else { + $revealReset.addClass('is-hidden'); + } + if (e.keyCode === 27) { + $revealCancel.click(); + } + }); + +})(jQuery); + +jQuery(function () { + 'use strict'; + + var pathname = window.location.pathname; + + // Check the url, see which links match and make them active + jQuery('#nav-user a, #nav-main a, #nav-main-side a, #nav-footer a') + .each(function () { + var navLink = jQuery(this) + .attr('href') + .replace(/\//g, '') + .replace(/\./g, ''); + if (pathname.toLowerCase().indexOf(navLink) >= 0) { + jQuery(this).addClass('is-active'); + } + }); + +}); + +'use strict'; + +zanata.createNS('zanata.tabs'); + +zanata.tabs = (function ($) { + + var activate = function (el) { + + var $this = $(el), + targetHash = $this.attr('href'), + targetID = targetHash.replace('#', ''), + $parent = $this.closest('.js-tabs'); + // data-content attribute should have a selector for the + // content container for the tab + if($this.is('[data-content]')) { + targetHash = $this.attr('data-content'); + } + if (!$this.parent().hasClass('is-active')) { + // Remove all is-active classes + $parent + .find('> .js-tabs-content > li, > .js-tabs-nav > li > a') + .removeClass('is-active'); + // Add relevant is-active classes + $this.blur().addClass('is-active'); + // Add hashed class so we can remove ID to change the hash + $(targetHash) + .addClass('is-active'); + // When changing tabs check for panels and resize to fit browser + if ($(targetHash).find('.js-panel__body').length > 0) { + zanata.panel.init(); + } + } + + }; + + var init = function () { + + $('.js-tabs').on('click', '.js-tabs-nav > li > a', function (e) { + e.preventDefault(); + activate(this); + }); + + }; + + // public API + return { + init: init, + activate: activate + }; + +})(jQuery); + +jQuery(function () { + zanata.tabs.init(); +}); + +'use strict'; + +zanata.createNS('zanata.tooltip'); + +zanata.tooltip = (function ($) { + + // Private methods + var init = function (el) { + $(el).tooltip({ + placement: 'auto bottom', + container: 'body', + delay: { + show: '500', + hide: '100' + } + }); + }; + + var refresh = function (el, newTitle) { + $(el) + .tooltip('hide') + .attr('data-original-title', newTitle) + .tooltip('fixTitle') + .tooltip('show'); + }; + + // public API + return { + init: init, + refresh: refresh + }; + +})(jQuery); diff --git a/server/zanata-war/src/main/webapp/resources/zanata/multi-file-upload.xhtml b/server/zanata-war/src/main/webapp/resources/zanata/multi-file-upload.xhtml index 027eafcce4..856d75d784 100644 --- a/server/zanata-war/src/main/webapp/resources/zanata/multi-file-upload.xhtml +++ b/server/zanata-war/src/main/webapp/resources/zanata/multi-file-upload.xhtml @@ -149,7 +149,7 @@ site: http://www.fsf.org. value="#{msgs['jsf.upload.ClientUploadInstructions']}" escape="false"> + value="<a href="http://docs.zanata.org/en/release/client/commands/push/">"/>

    @@ -190,7 +190,7 @@ site: http://www.fsf.org. - + @@ -230,10 +230,10 @@ site: http://www.fsf.org.