Initial Contribution Signed-off-by: Jan Supol <jan.supol@oracle.com>
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..367c54d --- /dev/null +++ b/.gitignore
@@ -0,0 +1,31 @@ +# maven noise +target/ + +# gradle noise +.gradle + +# osx noise +.DS_Store +profile + +# IntelliJ Idea noise +.idea +*.iws +*.ipr +*.iml +*.releaseBackup +atlassian-ide-plugin.xml + +# NB noise +nbactions.xml +nb-configuration.xml + +# Eclipse noise +.settings +.settings/* +.project +.classpath + +# Maven plugins noise +dependency-reduced-pom.xml +pom.xml.versionsBackup
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f42b9db --- /dev/null +++ b/CONTRIBUTING.md
@@ -0,0 +1,64 @@ +[//]: # " Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Public License v. 2.0, which is available at " +[//]: # " http://www.eclipse.org/legal/epl-2.0. " +[//]: # " " +[//]: # " This Source Code may also be made available under the following Secondary " +[//]: # " Licenses when the conditions for such availability set forth in the " +[//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " +[//]: # " version 2 with the GNU Classpath Exception, which is available at " +[//]: # " https://www.gnu.org/software/classpath/license.html. " +[//]: # " " +[//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " + +# Contributing to Eclipse Jersey + +Thanks for your interest in this project. + +## Project description + +Eclipse Jersey is a REST framework that provides a JAX-RS (JSR-370) implementation and more. +Jersey provides its own APIs that extend the JAX-RS toolkit with additional features and utilities +to further simplify RESTful service and client development. Jersey also exposes numerous extension +SPIs so that developers may extend Jersey to best suit their needs. + +Goals of Jersey project can be summarized in the following points: +* Track the JAX-RS API and provide regular releases of production quality implementations that ships with GlassFish; + +* Provide APIs to extend Jersey & Build a community of users and developers; and finally + +* Make it easy to build RESTful Web services utilising Java and the Java Virtual Machine. + +## Developer resources + +Information regarding source code management, builds, coding standards, and +more. + +* https://projects.eclipse.org/projects/ee4j.jersey/developer + +The project maintains the following source code repositories + +* https://github.com/eclipse-ee4j/jersey + +## Eclipse Contributor Agreement + +Before your contribution can be accepted by the project team contributors must +electronically sign the Eclipse Contributor Agreement (ECA). + +* http://www.eclipse.org/legal/ECA.php + +Commits that are provided by non-committers must have a Signed-off-by field in +the footer indicating that the author is aware of the terms by which the +contribution has been provided to the project. The non-committer must +additionally have an Eclipse Foundation account and must have a signed Eclipse +Contributor Agreement (ECA) on file. + +For more information, please see the Eclipse Committer Handbook: +https://www.eclipse.org/projects/handbook/#resources-commit + +## Contact + +Contact the project developers via the project's "dev" list. + +* https://dev.eclipse.org/mailman/listinfo/jersey-dev
diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..345fc37 --- /dev/null +++ b/LICENSE.md
@@ -0,0 +1,637 @@ +# Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + + "Contributor" means any person or entity that Distributes the Program. + + "Licensed Patents" mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone + or when combined with the Program. + + "Program" means the Contributions Distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement + or any Secondary License (as applicable), including Contributors. + + "Derivative Works" shall mean any work, whether in Source Code or other + form, that is based on (or derived from) the Program and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. + + "Modified Works" shall mean any work in Source Code or other form that + results from an addition to, deletion from, or modification of the + contents of the Program, including, for purposes of clarity any new file + in Source Code form that contains any contents of the Program. Modified + Works shall not include works that contain only declarations, + interfaces, types, classes, structures, or files of the Program solely + in each case in order to link to, bind by name, or subclass the Program + or Modified Works thereof. + + "Distribute" means the acts of a) distributing or b) making available + in any manner that enables the transfer of a copy. + + "Source Code" means the form of a Program preferred for making + modifications, including but not limited to software source code, + documentation source, and configuration files. + + "Secondary License" means either the GNU General Public License, + Version 2.0, or any later versions of that license, including any + exceptions or additional permissions as identified by the initial + Contributor. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + + 3. REQUIREMENTS + + 3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + + 3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + + 3.3 Contributors may not remove or alter any copyright, patent, + trademark, attribution notices, disclaimers of warranty, or limitations + of liability ("notices") contained within the Program from any copy of + the Program which they Distribute, provided that Contributors may add + their own appropriate notices. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product + offering should do so in a manner which does not create potential + liability for other Contributors. Therefore, if a Contributor includes + the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and indemnify every + other Contributor ("Indemnified Contributor") against any losses, + damages and costs (collectively "Losses") arising from claims, lawsuits + and other legal actions brought by a third party against the Indemnified + Contributor to the extent caused by the acts or omissions of such + Commercial Contributor in connection with its distribution of the Program + in a commercial product offering. The obligations in this section do not + apply to any claims or Losses relating to any actual or alleged + intellectual property infringement. In order to qualify, an Indemnified + Contributor must: a) promptly notify the Commercial Contributor in + writing of such claim, and b) allow the Commercial Contributor to control, + and cooperate with the Commercial Contributor in, the defense and any + related settlement negotiations. The Indemnified Contributor may + participate in any such claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those performance + claims and warranties, and if a court requires any other Contributor to + pay any damages as a result, the Commercial Contributor must pay + those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR + IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF + TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE. Each Recipient is solely responsible for determining the + appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement, + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs + or equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT + PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS + SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST + PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE + EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further + action by the parties hereto, such provision shall be reformed to the + minimum extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that the + Program itself (excluding combinations of the Program with other software + or hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it + fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of + time after becoming aware of such noncompliance. If all Recipient's + rights under this Agreement terminate, Recipient agrees to cease use + and distribution of the Program as soon as reasonably practicable. + However, Recipient's obligations under this Agreement and any licenses + granted by Recipient relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and + may only be modified in the following manner. The Agreement Steward + reserves the right to publish new versions (including revisions) of + this Agreement from time to time. No one other than the Agreement + Steward has the right to modify this Agreement. The Eclipse Foundation + is the initial Agreement Steward. The Eclipse Foundation may assign the + responsibility to serve as the Agreement Steward to a suitable separate + entity. Each new version of the Agreement will be given a distinguishing + version number. The Program (including Contributions) may always be + Distributed subject to the version of the Agreement under which it was + received. In addition, after a new version of the Agreement is published, + Contributor may elect to Distribute the Program (including its + Contributions) under the new version. + + Except as expressly stated in Sections 2(a) and 2(b) above, Recipient + receives no rights or licenses to the intellectual property of any + Contributor under this Agreement, whether expressly, by implication, + estoppel or otherwise. All rights in the Program not expressly granted + under this Agreement are reserved. Nothing in this Agreement is intended + to be enforceable by any entity that is not a Contributor or Recipient. + No third-party beneficiary rights are created under this Agreement. + + Exhibit A - Form of Secondary Licenses Notice + + "This Source Code may also be made available under the following + Secondary Licenses when the conditions for such availability set forth + in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), + version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + +--- + +## The GNU General Public License (GPL) Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor + Boston, MA 02110-1335 + 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 Library 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 a brief idea of what it does. + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 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-1335 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 Library + General Public License instead of this License. + +--- + +## CLASSPATH EXCEPTION + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License version 2 cover the whole + combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent + modules, and to copy and distribute the resulting executable under + terms of your choice, provided that you also meet, for each linked + independent module, the terms and conditions of the license of that + module. An independent module is a module which is not derived from or + based on this library. If you modify this library, you may extend this + exception to your version of the library, but you are not obligated to + do so. If you do not wish to do so, delete this exception statement + from your version.
diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..d8d6b38 --- /dev/null +++ b/NOTICE.md
@@ -0,0 +1,113 @@ +# Notice for Jersey +This content is produced and maintained by the Eclipse Jersey project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Angular JS, v1.6.6 +* License MIT (http://www.opensource.org/licenses/mit-license.php) +* Project: http://angularjs.org +* Coyright: (c) 2010-2017 Google, Inc. + +aopalliance Version 1 +* License: all the source code provided by AOP Alliance is Public Domain. +* Project: http://aopalliance.sourceforge.net +* Copyright: Material in the public domain is not protected by copyright + +Bean Validation API 1.1.0.Final +* License: Apache License, 2.0 +* Project: http://beanvalidation.org/1.1/ +* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors +* by the @authors tag. + +Bootstrap v3.3.7 +* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) +* Project: http://getbootstrap.com +* Copyright: 2011-2016 Twitter, Inc + +CDI API Version 1.1 +* License: Apache License, 2.0 +* Project: http://www.seamframework.org/Weld +* Copyright 2010, Red Hat, Inc., and individual contributors by the @authors tag. + +Google Guava Version 18.0 +* License: Apache License, 2.0 +* Copyright (C) 2009 The Guava Authors + +javax.inject Version: 1 +* License: Apache License, 2.0 +* Copyright (C) 2009 The JSR-330 Expert Group + +Javassist Version 3.22.0-CR2 +* License: Apache License, 2.0 +* Project: http://www.javassist.org/ +* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. + +Java(TM) EE Interceptors 1.1 API Version 1.0.0.Beta1 +* License: LGPL 2.1 +* Copyright 2005, JBoss Inc., and individual contributors as indicated by the @authors tag. + +Jackson JAX-RS Providers Version 2.8.10 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jaxrs-providers +* Copyright: (c) 2009-2011 FasterXML, LLC. All rights reserved unless otherwise indicated. + +jQuery v1.12.4 +* License: jquery.org/license +* Project: jquery.org +* Copyright: (c) jQuery Foundation + +jQuery Barcode plugin 0.3 +* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) +* Project: http://www.pasella.it/projects/jQuery/barcode +* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com + +JSR-166 Extension - JEP 266 +* License: CC0 +* No copyright +* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ + +KineticJS, v4.7.1 +* License: MIT license (http://www.opensource.org/licenses/mit-license.php) +* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS +* Copyright: Eric Rowell + +org.objectweb.asm Version 5.0.4 +* License: Modified BSD (http://asm.objectweb.org/license.html) +* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. + +org.osgi.core version 4.2.0 +* License: Apache License, 2.0 +* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. + +org.glassfish.jersey.server.internal.monitoring.core +* License: Apache License, 2.0 +* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. +* Copyright 2010-2013 Coda Hale and Yammer, Inc.
diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc4f6ea --- /dev/null +++ b/README.md
@@ -0,0 +1,48 @@ +[//]: # " Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Public License v. 2.0, which is available at " +[//]: # " http://www.eclipse.org/legal/epl-2.0. " +[//]: # " " +[//]: # " This Source Code may also be made available under the following Secondary " +[//]: # " Licenses when the conditions for such availability set forth in the " +[//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " +[//]: # " version 2 with the GNU Classpath Exception, which is available at " +[//]: # " https://www.gnu.org/software/classpath/license.html. " +[//]: # " " +[//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " + +### About Jersey + +Jersey is a REST framework that provides JAX-RS Reference Implementation and more. +Jersey provides its own [APIs][jersey-api] that extend the JAX-RS toolkit with +additional features and utilities to further simplify RESTful service and client +development. Jersey also exposes numerous extension SPIs so that developers may +extend Jersey to best suit their needs. + +Goals of Jersey project can be summarized in the following points: + +* Track the JAX-RS API and provide regular releases of production quality + Reference Implementations that ships with GlassFish; +* Provide APIs to extend Jersey & Build a community of users and developers; + and finally +* Make it easy to build RESTful Web services utilising Java and the + Java Virtual Machine. + +### Licensing and Governance +Jersey is licensed under a dual license - [EPL 2.0 and GPL 2.0 with Class-path Exception](LICENSE.md). +That means you can choose which one of the two suits your needs better and use it under those terms. + +We use [contribution policy](CONTRIBUTING.md), which means we can only accept contributions under + the terms of [ECA][eca]. + +### More Info on Jersey +See the [Jersey website][jersey-web] to access Jersey documentation. If you run into any issues or have questions, +ask at [jersey-dev@eclipse.org][jersey-users], [StackOverflow][jersey-so] or file an issue on [Jersey GitHub Project][jersey-issues]. + +[eca]: http://www.eclipse.org/legal/ECA.php +[jersey-api]: https://jersey.github.io/apidocs/latest/jersey/index.html +[jersey-issues]: https://github.com/eclipse-ee4j/jersey/issues +[jersey-so]: http://stackoverflow.com/questions/tagged/jersey +[jersey-users]: mailto:jersey-dev@eclipse.org +[jersey-web]: https://projects.eclipse.org/projects/ee4j.jersey
diff --git a/archetypes/jersey-example-java8-webapp/README.MD b/archetypes/jersey-example-java8-webapp/README.MD new file mode 100644 index 0000000..ce0a151 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/README.MD
@@ -0,0 +1,47 @@ +[//]: # " Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Public License v. 2.0, which is available at " +[//]: # " http://www.eclipse.org/legal/epl-2.0. " +[//]: # " " +[//]: # " This Source Code may also be made available under the following Secondary " +[//]: # " Licenses when the conditions for such availability set forth in the " +[//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " +[//]: # " version 2 with the GNU Classpath Exception, which is available at " +[//]: # " https://www.gnu.org/software/classpath/license.html. " +[//]: # " " +[//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " + +Jersey (Java8) WebApp Example Archetype. +========================================================== + +This module defines skeleton maven project (archetype) for developing new Java SE-based and Servlet-based examples for Jersey. + +Instructions +------------ + +- install this archetype into your local repository, `maven-archetype-plugin` updates your local `archetype-catalog.xml` +which is located in `.m2` directory. + +> mvn clean install + +- run generation your new project using: + +> mvn archetype:generate -DarchetypeCatalog=local + +- maven provides you a set of your local installed maven archetypes, choose this one +`org.glassfish.jersey.archetypes:jersey-example-java8-webapp` + +- fill in all input fields properly, e.g. + +``` +groupId: org.glassfish.jersey.examples (default value) +artifactId: my-example +version: Just use a release version - e.g. "2.20" +package: Use org.glassfish.jersey.examples.my-example +projectAuthor: Use "Name Surname (name.surname at mycompany.com)" +projectDescription: "My New Example using Jersey." +projectName: "My Jersey Example" +``` + +- confirm the project generation and feel free to start coding!
diff --git a/archetypes/jersey-example-java8-webapp/pom.xml b/archetypes/jersey-example-java8-webapp/pom.xml new file mode 100644 index 0000000..eb42c23 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/pom.xml
@@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.glassfish.jersey.archetypes</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-example-java8-webapp</artifactId> + <packaging>maven-archetype</packaging> + + <name>jersey-example-java8-webapp</name> + <description>Jersey (Java8) WebApp Example Archetype.</description> + + <build> + <resources> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> +</project>
diff --git a/archetypes/jersey-example-java8-webapp/src/main/resources/META-INF/maven/archetype-metadata.xml b/archetypes/jersey-example-java8-webapp/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 0000000..26f22f1 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<archetype-descriptor xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0 + http://maven.apache.org/xsd/archetype-descriptor-1.0.0.xsd" name="jersey-java8-webapp-archetype" + xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + <requiredProperties> + <requiredProperty key="groupId"> + <defaultValue>org.glassfish.jersey.examples</defaultValue> + </requiredProperty> + <requiredProperty key="package"> + <defaultValue /> + </requiredProperty> + <requiredProperty key="version"> + <defaultValue>${version}</defaultValue> + </requiredProperty> + <requiredProperty key="projectAuthor" /> + <requiredProperty key="projectName" /> + <requiredProperty key="projectDescription" /> + </requiredProperties> + + <fileSets> + <fileSet filtered="true" packaged="true" encoding="UTF-8"> + <directory>src/main/java</directory> + <includes> + <include>**/*.java</include> + </includes> + </fileSet> + <fileSet encoding="UTF-8"> + <directory>src/main/resources</directory> + </fileSet> + <fileSet filtered="true" packaged="true" encoding="UTF-8"> + <directory>src/test/java</directory> + <includes> + <include>**/*.java</include> + </includes> + </fileSet> + <fileSet filtered="true" encoding="UTF-8"> + <directory/> + <includes> + <include>README.MD</include> + </includes> + </fileSet> + </fileSets> +</archetype-descriptor>
diff --git a/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/README.MD b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/README.MD new file mode 100644 index 0000000..53e1288 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/README.MD
@@ -0,0 +1,47 @@ +[//]: # " Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Public License v. 2.0, which is available at " +[//]: # " http://www.eclipse.org/legal/epl-2.0. " +[//]: # " " +[//]: # " This Source Code may also be made available under the following Secondary " +[//]: # " Licenses when the conditions for such availability set forth in the " +[//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " +[//]: # " version 2 with the GNU Classpath Exception, which is available at " +[//]: # " https://www.gnu.org/software/classpath/license.html. " +[//]: # " " +[//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " + +${projectName} +========================================================== + +This example demonstrates how to ... + +Contents +-------- + +The mapping of the URI path space is presented in the following table: + +URI path | Resource class | HTTP methods +------------------------------------------ | ------------------------- | -------------- +**_/resource_** | JerseyResource | GET + +Sample Response +--------------- + +```javascript + +``` + +Running the Example +------------------- + +Run the example using [Grizzly](http://grizzly.java.net/) container as follows: + +> mvn clean compile exec:java + +Run the example using Jetty as follows: + +> mvn clean package jetty:run + +- <http://localhost:8080/base/jersey-resource>
diff --git a/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/pom.xml b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 0000000..b3e9f15 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>webapp-example-parent</artifactId> + <relativePath>../webapp-example-parent/pom.xml</relativePath> + <version>${version}</version> + </parent> + + <!-- Use in the case when Jetty is not needed + <parent> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>project</artifactId> + <version>${version}</version> + </parent> + --> + + <artifactId>${artifactId}</artifactId> + <packaging>war</packaging> + <name>${projectName}</name> + + <description>${projectDescription}</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <configuration> + <mainClass>${package}.App</mainClass> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <inherited>true</inherited> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + <showWarnings>false</showWarnings> + <fork>false</fork> + </configuration> + </plugin> + + <!-- Run the application using "mvn jetty:run" --> + <plugin> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-maven-plugin</artifactId> + <!-- TODO: Remove the version when JERSEY-2743 is resolved. --> + <version>9.2.6.v20141205</version> + <configuration> + <scanIntervalSeconds>5</scanIntervalSeconds> + <stopPort>9999</stopPort> + <stopKey>STOP</stopKey> + <webApp> + <contextPath>/</contextPath> + <webInfIncludeJarPattern>.*/.*jersey-[^/]\.jar$</webInfIncludeJarPattern> + </webApp> + <systemProperties> + <systemProperty> + <name>jetty.port</name> + <value>${jersey.config.test.container.port}</value> + </systemProperty> + </systemProperties> + <war>${project.build.directory}/${project.build.finalName}.war</war> + </configuration> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>release</id> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>xml-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <properties> + <java.version>1.8</java.version> + <jersey.config.test.container.port>8080</jersey.config.test.container.port> + </properties> +</project>
diff --git a/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/src/main/java/App.java b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/src/main/java/App.java new file mode 100644 index 0000000..f692853 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/src/main/java/App.java
@@ -0,0 +1,59 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package ${package}; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; + +import org.glassfish.grizzly.http.server.HttpServer; + +/** + * @author ${projectAuthor} + */ +public class App { + + private static final URI BASE_URI = URI.create("http://localhost:8080/base/"); + + public static final String ROOT_PATH = "resource-path"; + + public static void main(String[] args) { + try { + System.out.println("${projectName}"); + + final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(BASE_URI, new MyApplication(), false); + Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow)); + server.start(); + + System.out.println(String.format( + "Application started.${symbol_escape}n" + + "Try out %s%s${symbol_escape}n" + + "Stop the application using CTRL+C", + BASE_URI, ROOT_PATH)); + + Thread.currentThread().join(); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex); + } + } +}
diff --git a/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/src/main/java/MyApplication.java b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/src/main/java/MyApplication.java new file mode 100644 index 0000000..a4e0655 --- /dev/null +++ b/archetypes/jersey-example-java8-webapp/src/main/resources/archetype-resources/src/main/java/MyApplication.java
@@ -0,0 +1,37 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) +/* + * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package ${package}; + +import javax.ws.rs.ApplicationPath; + +import org.glassfish.jersey.filter.LoggingFilter; +import org.glassfish.jersey.server.ResourceConfig; + +/** + * @author ${projectAuthor} + */ +@ApplicationPath("/") +public class MyApplication extends ResourceConfig { + + public MyApplication() { + register(LoggingFilter.class); + } + +}
diff --git a/archetypes/jersey-heroku-webapp/pom.xml b/archetypes/jersey-heroku-webapp/pom.xml new file mode 100644 index 0000000..0a336a7 --- /dev/null +++ b/archetypes/jersey-heroku-webapp/pom.xml
@@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.glassfish.jersey.archetypes</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + <packaging>maven-archetype</packaging> + + <artifactId>jersey-heroku-webapp</artifactId> + <name>jersey-archetype-heroku-webapp</name> + <description> + An archetype which contains a quick start Jersey-based web application project capable to run on Heroku. + </description> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>2.5</version> + <configuration> + <escapeString>\</escapeString> + </configuration> + </plugin> + </plugins> + <resources> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> +</project>
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/META-INF/maven/archetype-metadata.xml b/archetypes/jersey-heroku-webapp/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 0000000..81cd89c --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<archetype-descriptor + name="jersey-heroku-webapp" + xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0 http://maven.apache.org/xsd/archetype-descriptor-1.0.0.xsd"> + + <fileSets> + <fileSet filtered="true" packaged="false" encoding="UTF-8"> + <directory/> + <includes> + <include>Procfile</include> + <include>system.properties</include> + </includes> + </fileSet> + <fileSet filtered="true" packaged="true" encoding="UTF-8"> + <directory>src/main/java</directory> + <includes> + <include>**/*</include> + </includes> + </fileSet> + <fileSet filtered="true" packaged="false" encoding="UTF-8"> + <directory>src/main/webapp</directory> + <includes> + <include>**/*</include> + </includes> + </fileSet> + <fileSet filtered="true" packaged="true" encoding="UTF-8"> + <directory>src/test/java</directory> + <includes> + <include>**/*</include> + </includes> + </fileSet> + </fileSets> +</archetype-descriptor>
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/Procfile b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/Procfile new file mode 100644 index 0000000..f5f8bdc --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/Procfile
@@ -0,0 +1 @@ +web: java -cp target/classes:target/dependency/* ${package}.heroku.Main \ No newline at end of file
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 0000000..e34d3ce --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,111 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <groupId>\${groupId}</groupId> + <artifactId>\${artifactId}</artifactId> + <packaging>war</packaging> + <version>\${version}</version> + <name>\${artifactId}</name> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.glassfish.jersey</groupId> + <artifactId>jersey-bom</artifactId> + <version>\${jersey.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + </dependency> + + <!-- uncomment this to get JSON support + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-binding</artifactId> + </dependency> --> + + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + <version>\${jetty.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-webapp</artifactId> + <version>\${jetty.version}</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-bundle</artifactId> + <type>pom</type> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <finalName>\${artifactId}</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.5.1</version> + <inherited>true</inherited> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <id>copy-dependencies</id> + <phase>package</phase> + <goals> + <goal>copy-dependencies</goal> + </goals> + <configuration> + <includeScope>compile</includeScope> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-maven-plugin</artifactId> + <version>\${jetty.version}</version> + <configuration> + <contextPath>/</contextPath> + <webApp> + <contextPath>/</contextPath> + <webInfIncludeJarPattern>.*/.*jersey-[^/]\.jar$</webInfIncludeJarPattern> + </webApp> + <war>\${project.build.directory}/\${project.build.finalName}.war</war> + </configuration> + </plugin> + </plugins> + </build> + + <properties> + <jersey.version>${project.version}</jersey.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <jetty.version>9.0.6.v20130930</jetty.version> + </properties> +</project>
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/java/MyResource.java b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/java/MyResource.java new file mode 100644 index 0000000..9da0396 --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/java/MyResource.java
@@ -0,0 +1,25 @@ +package ${package}; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * Root resource (exposed at "myresource" path) + */ +@Path("myresource") +public class MyResource { + + /** + * Method handling HTTP GET requests. The returned object will be sent + * to the client as "text/plain" media type. + * + * @return String that will be returned as a text/plain response. + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getIt() { + return "Hello, Heroku!"; + } +}
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/java/heroku/Main.java b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/java/heroku/Main.java new file mode 100644 index 0000000..06594bf --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/java/heroku/Main.java
@@ -0,0 +1,40 @@ +package ${package}.heroku; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.webapp.WebAppContext; + +/** + * This class launches the web application in an embedded Jetty container. This is the entry point to your application. The Java + * command that is used for launching should fire this main method. + */ +public class Main { + + public static void main(String[] args) throws Exception{ + // The port that we should run on can be set into an environment variable + // Look for that variable and default to 8080 if it isn't there. + String webPort = System.getenv("PORT"); + if (webPort == null || webPort.isEmpty()) { + webPort = "8080"; + } + + final Server server = new Server(Integer.valueOf(webPort)); + final WebAppContext root = new WebAppContext(); + + root.setContextPath("/"); + // Parent loader priority is a class loader setting that Jetty accepts. + // By default Jetty will behave like most web containers in that it will + // allow your application to replace non-server libraries that are part of the + // container. Setting parent loader priority to true changes this behavior. + // Read more here: http://wiki.eclipse.org/Jetty/Reference/Jetty_Classloading + root.setParentLoaderPriority(true); + + final String webappDirLocation = "src/main/webapp/"; + root.setDescriptor(webappDirLocation + "/WEB-INF/web.xml"); + root.setResourceBase(webappDirLocation); + + server.setHandler(root); + + server.start(); + server.join(); + } +}
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..8826a46 --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>Jersey Web Application</servlet-name> + <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> + <init-param> + <param-name>jersey.config.server.provider.packages</param-name> + <param-value>${package}</param-value> + </init-param> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>Jersey Web Application</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> +</web-app>
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/test/java/MyResourceTest.java b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/test/java/MyResourceTest.java new file mode 100644 index 0000000..f0c141c --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/src/test/java/MyResourceTest.java
@@ -0,0 +1,29 @@ +package ${package}; + +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +import ${package}.MyResource; + +public class MyResourceTest extends JerseyTest { + + @Override + protected Application configure() { + return new ResourceConfig(MyResource.class); + } + + /** + * Test to see that the message "Got it!" is sent in the response. + */ + @Test + public void testGetIt() { + final String responseMsg = target().path("myresource").request().get(String.class); + + assertEquals("Hello, Heroku!", responseMsg); + } +}
diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/system.properties b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/system.properties new file mode 100644 index 0000000..4d46ac0 --- /dev/null +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/system.properties
@@ -0,0 +1 @@ +java.runtime.version=1.7 \ No newline at end of file
diff --git a/archetypes/jersey-quickstart-grizzly2/pom.xml b/archetypes/jersey-quickstart-grizzly2/pom.xml new file mode 100644 index 0000000..a005cd9 --- /dev/null +++ b/archetypes/jersey-quickstart-grizzly2/pom.xml
@@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>org.glassfish.jersey.archetypes</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + <artifactId>jersey-quickstart-grizzly2</artifactId> + <packaging>maven-archetype</packaging> + <modelVersion>4.0.0</modelVersion> + <description> + An archetype which contains a quick start Jersey project based on Grizzly container. + </description> + <name>jersey-archetype-grizzly2</name> + + <build> + <resources> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>2.5</version> + <configuration> + <escapeString>\</escapeString> + </configuration> + </plugin> + </plugins> + + </build> +</project>
diff --git a/archetypes/jersey-quickstart-grizzly2/src/main/resources/META-INF/archetype.xml b/archetypes/jersey-quickstart-grizzly2/src/main/resources/META-INF/archetype.xml new file mode 100644 index 0000000..8a4f997 --- /dev/null +++ b/archetypes/jersey-quickstart-grizzly2/src/main/resources/META-INF/archetype.xml
@@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<archetype> + <id>jersey-quickstart-grizzly2</id> + <sources> + <source>src/main/java/Main.java</source> + <source>src/main/java/MyResource.java</source> + </sources> + <testSources> + <source>src/test/java/MyResourceTest.java</source> + </testSources> +</archetype>
diff --git a/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/pom.xml b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 0000000..bacd3bc --- /dev/null +++ b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,82 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <groupId>\${groupId}</groupId> + <artifactId>\${artifactId}</artifactId> + <packaging>jar</packaging> + <version>\${version}</version> + <name>\${artifactId}</name> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.glassfish.jersey</groupId> + <artifactId>jersey-bom</artifactId> + <version>\${jersey.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + </dependency> + + <!-- uncomment this to get JSON support: + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-binding</artifactId> + </dependency> + --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.9</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.5.1</version> + <inherited>true</inherited> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.2.1</version> + <executions> + <execution> + <goals> + <goal>java</goal> + </goals> + </execution> + </executions> + <configuration> + <mainClass>\${package}.Main</mainClass> + </configuration> + </plugin> + </plugins> + </build> + + <properties> + <jersey.version>${project.version}</jersey.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> +</project>
diff --git a/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/main/java/Main.java b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/main/java/Main.java new file mode 100644 index 0000000..a97cf63 --- /dev/null +++ b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/main/java/Main.java
@@ -0,0 +1,45 @@ +package $package; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.server.ResourceConfig; + +import java.io.IOException; +import java.net.URI; + +/** + * Main class. + * + */ +public class Main { + // Base URI the Grizzly HTTP server will listen on + public static final String BASE_URI = "http://localhost:8080/myapp/"; + + /** + * Starts Grizzly HTTP server exposing JAX-RS resources defined in this application. + * @return Grizzly HTTP server. + */ + public static HttpServer startServer() { + // create a resource config that scans for JAX-RS resources and providers + // in $package package + final ResourceConfig rc = new ResourceConfig().packages("$package"); + + // create and start a new instance of grizzly http server + // exposing the Jersey application at BASE_URI + return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc); + } + + /** + * Main method. + * @param args + * @throws IOException + */ + public static void main(String[] args) throws IOException { + final HttpServer server = startServer(); + System.out.println(String.format("Jersey app started with WADL available at " + + "%sapplication.wadl\nHit enter to stop it...", BASE_URI)); + System.in.read(); + server.stop(); + } +} +
diff --git a/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/main/java/MyResource.java b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/main/java/MyResource.java new file mode 100644 index 0000000..53beee1 --- /dev/null +++ b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/main/java/MyResource.java
@@ -0,0 +1,25 @@ +package $package; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * Root resource (exposed at "myresource" path) + */ +@Path("myresource") +public class MyResource { + + /** + * Method handling HTTP GET requests. The returned object will be sent + * to the client as "text/plain" media type. + * + * @return String that will be returned as a text/plain response. + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getIt() { + return "Got it!"; + } +}
diff --git a/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/test/java/MyResourceTest.java b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/test/java/MyResourceTest.java new file mode 100644 index 0000000..6986107 --- /dev/null +++ b/archetypes/jersey-quickstart-grizzly2/src/main/resources/archetype-resources/src/test/java/MyResourceTest.java
@@ -0,0 +1,48 @@ +package $package; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.grizzly.http.server.HttpServer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class MyResourceTest { + + private HttpServer server; + private WebTarget target; + + @Before + public void setUp() throws Exception { + // start the server + server = Main.startServer(); + // create the client + Client c = ClientBuilder.newClient(); + + // uncomment the following line if you want to enable + // support for JSON in the client (you also have to uncomment + // dependency on jersey-media-json module in pom.xml and Main.startServer()) + // -- + // c.configuration().enable(new org.glassfish.jersey.media.json.JsonJaxbFeature()); + + target = c.target(Main.BASE_URI); + } + + @After + public void tearDown() throws Exception { + server.stop(); + } + + /** + * Test to see that the message "Got it!" is sent in the response. + */ + @Test + public void testGetIt() { + String responseMsg = target.path("myresource").request().get(String.class); + assertEquals("Got it!", responseMsg); + } +}
diff --git a/archetypes/jersey-quickstart-webapp/pom.xml b/archetypes/jersey-quickstart-webapp/pom.xml new file mode 100644 index 0000000..47e666b --- /dev/null +++ b/archetypes/jersey-quickstart-webapp/pom.xml
@@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <groupId>org.glassfish.jersey.archetypes</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <packaging>maven-archetype</packaging> + <description> + An archetype which contains a quick start Jersey-based web application project. + </description> + <artifactId>jersey-quickstart-webapp</artifactId> + <name>jersey-archetype-webapp</name> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>2.5</version> + <configuration> + <escapeString>\</escapeString> + </configuration> + </plugin> + </plugins> + <resources> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> +</project>
diff --git a/archetypes/jersey-quickstart-webapp/src/main/resources/META-INF/archetype.xml b/archetypes/jersey-quickstart-webapp/src/main/resources/META-INF/archetype.xml new file mode 100644 index 0000000..cea1eea --- /dev/null +++ b/archetypes/jersey-quickstart-webapp/src/main/resources/META-INF/archetype.xml
@@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<archetype> + <id>jersey-quickstart-webapp</id> + <sources> + <source>src/main/java/MyResource.java</source> + </sources> + <resources> + <resource>src/main/webapp/index.jsp</resource> + <resource>src/main/webapp/WEB-INF/web.xml</resource> + </resources> +</archetype>
diff --git a/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/pom.xml b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 0000000..49c9e7d --- /dev/null +++ b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,62 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <groupId>\${groupId}</groupId> + <artifactId>\${artifactId}</artifactId> + <packaging>war</packaging> + <version>\${version}</version> + <name>\${artifactId}</name> + + <build> + <finalName>\${artifactId}</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.5.1</version> + <inherited>true</inherited> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + </plugins> + </build> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.glassfish.jersey</groupId> + <artifactId>jersey-bom</artifactId> + <version>\${jersey.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <!-- use the following artifactId if you don't need servlet 2.x compatibility --> + <!-- artifactId>jersey-container-servlet</artifactId --> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + </dependency> + <!-- uncomment this to get JSON support + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-binding</artifactId> + </dependency> + --> + </dependencies> + <properties> + <jersey.version>${project.version}</jersey.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> +</project>
diff --git a/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/java/MyResource.java b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/java/MyResource.java new file mode 100644 index 0000000..53beee1 --- /dev/null +++ b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/java/MyResource.java
@@ -0,0 +1,25 @@ +package $package; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * Root resource (exposed at "myresource" path) + */ +@Path("myresource") +public class MyResource { + + /** + * Method handling HTTP GET requests. The returned object will be sent + * to the client as "text/plain" media type. + * + * @return String that will be returned as a text/plain response. + */ + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getIt() { + return "Got it!"; + } +}
diff --git a/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..7c85b3a --- /dev/null +++ b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This web.xml file is not required when using Servlet 3.0 container, + see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html --> +<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> + <servlet> + <servlet-name>Jersey Web Application</servlet-name> + <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> + <init-param> + <param-name>jersey.config.server.provider.packages</param-name> + <param-value>$package</param-value> + </init-param> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>Jersey Web Application</servlet-name> + <url-pattern>/webapi/*</url-pattern> + </servlet-mapping> +</web-app>
diff --git a/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/webapp/index.jsp b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/webapp/index.jsp new file mode 100644 index 0000000..a064b45 --- /dev/null +++ b/archetypes/jersey-quickstart-webapp/src/main/resources/archetype-resources/src/main/webapp/index.jsp
@@ -0,0 +1,8 @@ +<html> +<body> + <h2>Jersey RESTful Web Application!</h2> + <p><a href="webapi/myresource">Jersey resource</a> + <p>Visit <a href="http://jersey.java.net">Project Jersey website</a> + for more information on Jersey! +</body> +</html>
diff --git a/archetypes/pom.xml b/archetypes/pom.xml new file mode 100644 index 0000000..8963ace --- /dev/null +++ b/archetypes/pom.xml
@@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.glassfish.jersey</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.archetypes</groupId> + <artifactId>project</artifactId> + <packaging>pom</packaging> + <name>jersey-archetypes</name> + <description> + A module containing archetypes for generating Jersey-based applications. + </description> + + <modules> + <module>jersey-heroku-webapp</module> + <module>jersey-quickstart-grizzly2</module> + <module>jersey-quickstart-webapp</module> + <module>jersey-example-java8-webapp</module> + </modules> + + <build> + <extensions> + <extension> + <groupId>org.apache.maven.archetype</groupId> + <artifactId>archetype-packaging</artifactId> + <version>${archetype.mvn.plugin.version}</version> + </extension> + </extensions> + + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-archetype-plugin</artifactId> + <version>${archetype.mvn.plugin.version}</version> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>release</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>${gpg.mvn.plugin.version}</version> + <executions> + <execution> + <id>sign-artifact</id> + <phase>verify</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> +</project>
diff --git a/bom/pom.xml b/bom/pom.xml new file mode 100644 index 0000000..50ee2a4 --- /dev/null +++ b/bom/pom.xml
@@ -0,0 +1,454 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>net.java</groupId> + <artifactId>jvnet-parent</artifactId> + <version>4</version> + <relativePath /> + </parent> + + <groupId>org.glassfish.jersey</groupId> + <artifactId>jersey-bom</artifactId> + <version>2.28-SNAPSHOT</version> + <packaging>pom</packaging> + <name>jersey-bom</name> + + <description>Jersey Bill of Materials (BOM)</description> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.bundles</groupId> + <artifactId>jaxrs-ri</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-apache-connector</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-grizzly-connector</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-jetty-connector</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-jdk-connector</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-netty-connector</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jetty-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jetty-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jdk-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-netty-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-simple-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers.glassfish</groupId> + <artifactId>jersey-gf-ejb</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-bean-validation</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-entity-filtering</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-metainf-services</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-mvc</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-mvc-bean-validation</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-mvc-freemarker</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-mvc-jsp</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-mvc-mustache</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-proxy-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-servlet-portability</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-spring4</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-declarative-linking</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext</groupId> + <artifactId>jersey-wadl-doclet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-weld2-se</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-cdi1x</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-cdi1x-transaction</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-cdi1x-validation</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-cdi1x-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-cdi1x-ban-custom-hk2-binding</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.rx</groupId> + <artifactId>jersey-rx-client-guava</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.rx</groupId> + <artifactId>jersey-rx-client-rxjava</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.ext.rx</groupId> + <artifactId>jersey-rx-client-rxjava2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-jaxb</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson1</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jettison</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-processing</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-binding</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-kryo</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-moxy</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-multipart</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-sse</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.security</groupId> + <artifactId>oauth1-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.security</groupId> + <artifactId>oauth1-server</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.security</groupId> + <artifactId>oauth1-signature</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.security</groupId> + <artifactId>oauth2-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-cdi2-se</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework</groupId> + <artifactId>jersey-test-framework-core</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-bundle</artifactId> + <version>${project.version}</version> + <type>pom</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-external</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-grizzly2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-inmemory</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-jdk-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-simple</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-jetty</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework</groupId> + <artifactId>jersey-test-framework-util</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.glassfish.copyright</groupId> + <artifactId>glassfish-copyright-maven-plugin</artifactId> + <version>1.28</version> + <configuration> + <excludeFile>etc/config/copyright-exclude</excludeFile> + <!--svn|mercurial|git - defaults to svn--> + <scm>git</scm> + <!-- turn on/off debugging --> + <debug>false</debug> + <!-- skip files not under SCM--> + <scmOnly>true</scmOnly> + <!-- turn off warnings --> + <warn>true</warn> + <!-- for use with repair --> + <update>false</update> + <!-- check that year is correct --> + <ignoreYear>false</ignoreYear> + <templateFile>etc/config/copyright.txt</templateFile> + </configuration> + </plugin> + </plugins> + </pluginManagement> + </build> + + <profiles> + <profile> + <id>project-info</id> + <activation> + <activeByDefault>false</activeByDefault> + </activation> + + <!-- placeholder required for site:stage --> + <distributionManagement> + <site> + <id>localhost</id> + <url>http://localhost</url> + </site> + </distributionManagement> + </profile> + <profile> + <id>release</id> + <activation> + <activeByDefault>false</activeByDefault> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>1.6.7</version> + <executions> + <execution> + <id>default-deploy</id> + <phase>deploy</phase> + <goals> + <goal>deploy</goal> + </goals> + </execution> + </executions> + <configuration> + <serverId>jvnet-nexus-staging</serverId> + <nexusUrl>https://maven.java.net/</nexusUrl> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> +</project>
diff --git a/bundles/README b/bundles/README new file mode 100644 index 0000000..4f2ac51 --- /dev/null +++ b/bundles/README
@@ -0,0 +1 @@ +Jersey distribution bundles. \ No newline at end of file
diff --git a/bundles/apidocs/pom.xml b/bundles/apidocs/pom.xml new file mode 100644 index 0000000..f02bb60 --- /dev/null +++ b/bundles/apidocs/pom.xml
@@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.glassfish.jersey.bundles</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>apidocs</artifactId> + <name>jersey-bundles-apidocs</name> + <packaging>jar</packaging> + + <dependencies> + <!-- CORE --> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + </dependency> + <!-- need to explicitly include the osgi dependency here + as it is scoped as provided in the jersey-common module --> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + </dependency> + <!-- CONTAINERS --> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${project.version}</version> + </dependency> + <!-- need to explicitly include the servlet dependency here + as it is scoped as provided in the jersey-servet modules --> + <dependency> + <groupId>org.glassfish</groupId> + <artifactId>javax.servlet</artifactId> + <version>3.1</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-simple-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jdk-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <!-- MEDIA --> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-moxy</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-multipart</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-sse</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <artifactId>maven-javadoc-plugin</artifactId> + <executions> + <execution> + <id>javadoc-jar</id> + <!--phase>package</phase--> + <goals> + <goal>jar</goal> + </goals> + <configuration> + <includeDependencySources>true</includeDependencySources> + <dependencySourceIncludes> + <dependencySourceInclude>org.glassfish.jersey.*:*</dependencySourceInclude> + </dependencySourceIncludes> + <excludePackageNames>*.internal.*</excludePackageNames> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>exclude-jar</id> + <goals> + <goal>remove-project-artifact</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project>
diff --git a/bundles/examples/pom.xml b/bundles/examples/pom.xml new file mode 100644 index 0000000..78e25b0 --- /dev/null +++ b/bundles/examples/pom.xml
@@ -0,0 +1,734 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.bundles</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-examples</artifactId> + <name>jersey-bundles-examples</name> + <packaging>pom</packaging> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bean-validation-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bean-validation-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bookmark</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bookmark</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bookmark-em</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bookmark-em</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bookstore-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>bookstore-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>cdi-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>cdi-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>clipboard</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>clipboard-programmatic</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>declarative-linking</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <!--Uncomment as soon as we build Jersey with Java8 by default--> + <!--<dependency>--> + <!--<groupId>org.glassfish.jersey.examples</groupId>--> + <!--<artifactId>default-method-java8-webapp</artifactId>--> + <!--<version>${project.version}</version>--> + <!--<classifier>project-src</classifier>--> + <!--<type>zip</type>--> + <!--</dependency>--> + <!--<dependency>--> + <!--<groupId>org.glassfish.jersey.examples</groupId>--> + <!--<artifactId>default-method-java8-webapp</artifactId>--> + <!--<version>${project.version}</version>--> + <!--<classifier>gf-project-src</classifier>--> + <!--<type>zip</type>--> + <!--</dependency>--> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>entity-filtering</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>entity-filtering-security</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>entity-filtering-selectable</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>exception-mapping</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>extended-wadl-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>extended-wadl-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>freemarker-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>freemarker-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>groovy</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-benchmark</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-programmatic</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-pure-jax-rs</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-spring-annotations</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-spring-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <!-- There is problem to run Spring example on GF - https://java.net/jira/browse/JERSEY-2032 + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-spring-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + --> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>helloworld-weld</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>http-patch</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>http-trace</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>https-clientserver-grizzly</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>https-server-glassfish</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>https-server-glassfish</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>jaxb</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>jaxrs-types-injection</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>jersey-ejb</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>jersey-ejb</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-jackson</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-jackson1</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-jettison</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-moxy</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-processing-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-processing-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>json-with-padding</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-beans-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-beans-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-client</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-client-simple-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-client-simple-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-client-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>managed-client-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>monitoring-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>multipart-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>multipart-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>oauth-client-twitter</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>oauth2-client-google-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>osgi-helloworld-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>osgi-http-service</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>reload</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>rx-client-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>rx-client-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>server-async</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>server-async-managed</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>server-async-standalone</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>server-sent-events-jersey</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>server-sent-events-jaxrs</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>servlet3-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>servlet3-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>shortener-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>shortener-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>simple-console</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>sparklines</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>sse-item-store-jersey-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>sse-item-store-jaxrs-webapp</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>sse-item-store-jersey-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>sse-item-store-jaxrs-webapp</artifactId> + <version>${project.version}</version> + <classifier>gf-project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>sse-twitter-aggregator</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>system-properties-example</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>tone-generator</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.examples</groupId> + <artifactId>xml-moxy</artifactId> + <version>${project.version}</version> + <classifier>project-src</classifier> + <type>zip</type> + </dependency> + + + </dependencies> + <build> + <finalName>${project.artifactId}</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <id>unpack-examples</id> + <phase>process-sources</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <stripVersion>true</stripVersion> + <includeGroupIds>org.glassfish.jersey.examples</includeGroupIds> + <classifier>project-src</classifier> + <type>zip</type> + <outputDirectory>${project.build.directory}/dependency/examples</outputDirectory> + </configuration> + </execution> + <execution> + <id>unpack-gf-examples</id> + <phase>process-sources</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <stripVersion>true</stripVersion> + <includeGroupIds>org.glassfish.jersey.examples</includeGroupIds> + <classifier>gf-project-src</classifier> + <type>zip</type> + <failOnMissingClassifierArtifact>false</failOnMissingClassifierArtifact> + <outputDirectory>${project.build.directory}/dependency/gf-examples</outputDirectory> + </configuration> + </execution> + <execution> + <id>unpack-wls-examples</id> + <phase>process-sources</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <stripVersion>true</stripVersion> + <includeGroupIds>org.glassfish.jersey.examples</includeGroupIds> + <classifier>wls-project-src</classifier> + <type>zip</type> + <failOnMissingClassifierArtifact>false</failOnMissingClassifierArtifact> + <outputDirectory>${project.build.directory}/dependency/wls-examples</outputDirectory> + </configuration> + </execution> + <execution> + <id>unpack-wls1213-examples</id> + <phase>process-sources</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <stripVersion>true</stripVersion> + <includeGroupIds>org.glassfish.jersey.examples</includeGroupIds> + <classifier>wls1213-project-src</classifier> + <type>zip</type> + <failOnMissingClassifierArtifact>false</failOnMissingClassifierArtifact> + <outputDirectory>${project.build.directory}/dependency/wls1213-examples</outputDirectory> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <descriptors> + <descriptor>src/main/assembly/examples-assembly.xml</descriptor> + <descriptor>src/main/assembly/gf-examples-assembly.xml</descriptor> + <descriptor>src/main/assembly/wls-examples-assembly.xml</descriptor> + <descriptor>src/main/assembly/wls1213-examples-assembly.xml</descriptor> + </descriptors> + </configuration> + <executions> + <execution> + <id>make-assembly</id> + <phase>package</phase> + <goals> + <goal>attached</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project>
diff --git a/bundles/examples/src/main/assembly/examples-assembly.xml b/bundles/examples/src/main/assembly/examples-assembly.xml new file mode 100644 index 0000000..37cef57 --- /dev/null +++ b/bundles/examples/src/main/assembly/examples-assembly.xml
@@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>all</id> + <includeBaseDirectory>false</includeBaseDirectory> + <formats> + <format>zip</format> + </formats> + <fileSets> + <fileSet> + <directory>target/dependency/examples</directory> + <outputDirectory>jersey/examples</outputDirectory> + </fileSet> + </fileSets> + <files> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>jersey</outputDirectory> + </file> + </files> +</assembly>
diff --git a/bundles/examples/src/main/assembly/gf-examples-assembly.xml b/bundles/examples/src/main/assembly/gf-examples-assembly.xml new file mode 100644 index 0000000..9ad77b9 --- /dev/null +++ b/bundles/examples/src/main/assembly/gf-examples-assembly.xml
@@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>gf</id> + <includeBaseDirectory>false</includeBaseDirectory> + <formats> + <format>zip</format> + </formats> + <fileSets> + <fileSet> + <directory>target/dependency/gf-examples</directory> + <outputDirectory>glassfish/jersey/examples</outputDirectory> + </fileSet> + </fileSets> + <files> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>.</outputDirectory> + </file> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>glassfish/jersey</outputDirectory> + </file> + </files> +</assembly>
diff --git a/bundles/examples/src/main/assembly/wls-examples-assembly.xml b/bundles/examples/src/main/assembly/wls-examples-assembly.xml new file mode 100644 index 0000000..c1c378b --- /dev/null +++ b/bundles/examples/src/main/assembly/wls-examples-assembly.xml
@@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>wls</id> + <includeBaseDirectory>false</includeBaseDirectory> + <formats> + <format>zip</format> + </formats> + <fileSets> + <fileSet> + <directory>target/dependency/wls-examples</directory> + <outputDirectory>wls/jersey/examples</outputDirectory> + <!-- exclude examples not working on WebLogic--> + <excludes> + <exclude>**/bookmark/**</exclude> <!-- uses persistence configured for GF --> + <exclude>**/https-server-glassfish/**</exclude> <!-- GF only, no tests to run anyway--> + <exclude>**/managed-beans-webapp/**</exclude> <!-- uses persistence configured for GF --> + </excludes> + </fileSet> + </fileSets> + <files> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>.</outputDirectory> + </file> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>wls/jersey</outputDirectory> + </file> + </files> +</assembly>
diff --git a/bundles/examples/src/main/assembly/wls1213-examples-assembly.xml b/bundles/examples/src/main/assembly/wls1213-examples-assembly.xml new file mode 100644 index 0000000..52db705 --- /dev/null +++ b/bundles/examples/src/main/assembly/wls1213-examples-assembly.xml
@@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>wls1213</id> + <includeBaseDirectory>false</includeBaseDirectory> + <formats> + <format>zip</format> + </formats> + <fileSets> + <fileSet> + <directory>target/dependency/wls1213-examples</directory> + <outputDirectory>wls1213/jersey/examples</outputDirectory> + <!-- exclude examples not working on WebLogic--> + <excludes> + <exclude>**/bookmark/**</exclude> <!-- uses persistence configured for GF --> + <exclude>**/https-server-glassfish/**</exclude> <!-- GF only, no tests to run anyway --> + <exclude>**/managed-beans-webapp/**</exclude> <!-- uses persistence configured for GF --> + <exclude>**/rx-client-java8-webapp/**</exclude> <!-- Java8 --> + <exclude>**/default-method-java8-webapp/**</exclude> <!-- Java8 --> + </excludes> + </fileSet> + </fileSets> + <files> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>.</outputDirectory> + </file> + <file> + <source>../../LICENSE.md</source> + <outputDirectory>wls1213/jersey</outputDirectory> + </file> + </files> +</assembly>
diff --git a/bundles/jaxrs-ri/pom.xml b/bundles/jaxrs-ri/pom.xml new file mode 100644 index 0000000..c78f2a7 --- /dev/null +++ b/bundles/jaxrs-ri/pom.xml
@@ -0,0 +1,451 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.glassfish.jersey.bundles</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jaxrs-ri</artifactId> + <name>jersey-bundles-jaxrs-ri</name> + <packaging>bundle</packaging> + + <description> + A bundle project producing JAX-RS RI bundles. The primary artifact is an "all-in-one" OSGi-fied JAX-RS RI bundle + (jaxrs-ri.jar). + Attached to that are two compressed JAX-RS RI archives. The first archive (jaxrs-ri.zip) consists of binary RI bits and + contains the API jar (under "api" directory), RI libraries (under "lib" directory) as well as all external + RI dependencies (under "ext" directory). The secondary archive (jaxrs-ri-src.zip) contains buildable JAX-RS RI source + bundle and contains the API jar (under "api" directory), RI sources (under "src" directory) as well as all external + RI dependencies (under "ext" directory). The second archive also contains "build.xml" ANT script that builds the RI + sources. To build the JAX-RS RI simply unzip the archive, cd to the created jaxrs-ri directory and invoke "ant" from + the command line. + </description> + + <dependencies> + <!-- JAX-RS API Sources--> + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + <version>${jaxrs.api.impl.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + + <!-- JAX-RS RI Binaries --> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-jaxb</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-binding</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-sse</artifactId> + <version>${project.version}</version> + </dependency> + + <!-- JAX-RS RI Sources --> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-jaxb</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${project.version}</version> + <classifier>sources</classifier> + <optional>true</optional> + </dependency> + + <!-- RI dependencies --> + <dependency> + <groupId>org.glassfish.hk2</groupId> + <artifactId>hk2-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.hk2</groupId> + <artifactId>hk2-locator</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.xml.bind</groupId> + <artifactId>jaxb-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.annotation</groupId> + <artifactId>javax.annotation-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.validation</groupId> + <artifactId>validation-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <version>${servlet3.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.persistence</groupId> + <artifactId>persistence-api</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + + <build> + <finalName>jaxrs-ri</finalName> + <resources> + <resource> + <directory>${generated.src.dir}</directory> + <excludes> + <exclude>**/*.java</exclude> + </excludes> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <inherited>false</inherited> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + <compilerArguments> + <!-- Do not warn about using sun.misc.Unsafe --> + <XDignore.symbol.file /> + </compilerArguments> + <showWarnings>false</showWarnings> + <fork>false</fork> + <excludes> + <exclude>module-info.java</exclude> + </excludes> + </configuration> + </plugin> + <!-- producing single jar bundle --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>${generated.src.dir}</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <executions> + <execution> + <id>unpack</id> + <phase>generate-sources</phase> + <goals> + <goal>unpack-dependencies</goal> + </goals> + <configuration> + <includeGroupIds>javax.ws.rs,org.glassfish.jersey.core,org.glassfish.jersey.containers,org.glassfish.jersey.jaxb,org.glassfish.jersey.inject</includeGroupIds> + <includeClassifiers>sources</includeClassifiers> + <outputDirectory>${generated.src.dir}</outputDirectory> + <excludes>**/NOTICE.md</excludes> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <configuration> + <instructions> + <Bundle-Name>${project.artifactId}</Bundle-Name> + <Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName> + <Specification-Version>${jaxrs.api.spec.version}</Specification-Version> + <Implementation-Version> + ${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.qualifier} + </Implementation-Version> + <Extension-Name>${project.artifactId}</Extension-Name> + <Export-Package> + javax.ws.rs.*;version=${jaxrs.api.impl.version}, + org.glassfish.jersey.*;version=${project.version}, + com.sun.research.ws.wadl.*;version=${project.version}, + jersey.repackaged.org.objectweb.asm.*;version=${project.version} + </Export-Package> + <Import-Package><![CDATA[ + javax.servlet.annotation.*;resolution:=optional;version="[2.4,5.0)", + javax.servlet.descriptor.*;resolution:=optional;version="[2.4,5.0)", + javax.servlet.*;version="[2.4,5.0)", + javax.persistence.*;resolution:=optional, + javax.validation.*;resolution:=optional;version="${range;[==,3);${javax.validation.api.version}}", + sun.misc.*;resolution:=optional, + * + ]]></Import-Package> + <Private-Package> + com.sun.research.ws.wadl + </Private-Package> + </instructions> + <unpackBundle>true</unpackBundle> + <excludeDependencies>*;scope=compile</excludeDependencies> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>3.1.0</version> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <filters> + <filter> + <artifact>*:*</artifact> <!-- jersey artifacts --> + <excludes> + <exclude>META-INF/NOTICE.md</exclude> + </excludes> + </filter> + <filter> + <artifact>*:*</artifact> <!-- 3rd party artifacts --> + <excludes> + <exclude>META-INF/DEPENDENCIES.txt</exclude> + <exclude>META-INF/LICENSE.md</exclude> + <exclude>javax/annotation/**</exclude> + <exclude>javax/decorator/**</exclude> + <exclude>javax/el/**</exclude> + <exclude>javax/enterprise/**</exclude> + <exclude>javax/interceptor/**</exclude> + </excludes> + </filter> + </filters> + </configuration> + </execution> + </executions> + </plugin> + <!-- producing zipped archives --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <id>make-binary-archive</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>${project.basedir}/src/main/assembly/assembly-bin.xml</descriptor> + </descriptors> + <appendAssemblyId>false</appendAssemblyId> + </configuration> + </execution> + <execution> + <id>make-source-archive</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptors> + <descriptor>${project.basedir}/src/main/assembly/assembly-src.xml</descriptor> + </descriptors> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-antrun-plugin</artifactId> + <executions> + <execution> + <id>xcopy</id> + <phase>package</phase> + <configuration> + <tasks> + <jar destfile="${project.build.directory}/${artifactId}.jar" update="true"> + <zipfileset dir="../.." includes="NOTICE.md" prefix="META-INF" /> + </jar> + <jar destfile="${project.build.directory}/${artifactId}-sources.jar" update="true"> + <zipfileset dir="../.." includes="NOTICE.md" prefix="META-INF" /> + </jar> + </tasks> + </configuration> + <goals> + <goal>run</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>licensee.src.bundle</id> + <activation> + <property> + <name>license.url</name> + </property> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>wagon-maven-plugin</artifactId> + <version>1.0-beta-4</version> + <inherited>false</inherited> + <executions> + <execution> + <id>get-license</id> + <phase>package</phase> + <goals> + <goal>download-single</goal> + </goals> + <configuration> + <url>${license.url}</url> + <fromFile>TLDA_SCSL_Licensees_License_Notice.txt</fromFile> + <toDir>${project.build.directory}/license</toDir> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <inherited>false</inherited> + <executions> + <execution> + <id>make-licensee-source-archive</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <finalName>jaxrs-ri-${project.version}-src-licensee</finalName> + <attach>false</attach> + <appendAssemblyId>false</appendAssemblyId> + <descriptors> + <descriptor>${project.basedir}/src/main/assembly/assembly-src-licensee.xml</descriptor> + </descriptors> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <properties> + <generated.src.dir>${basedir}/target/unpacked-src/main/java</generated.src.dir> + </properties> +</project>
diff --git a/bundles/jaxrs-ri/src/main/assembly/assembly-bin.xml b/bundles/jaxrs-ri/src/main/assembly/assembly-bin.xml new file mode 100644 index 0000000..6eced85 --- /dev/null +++ b/bundles/jaxrs-ri/src/main/assembly/assembly-bin.xml
@@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>binaries</id> + + <formats> + <format>zip</format> + <format>tar.gz</format> + <format>tar.bz2</format> + </formats> + + <includeBaseDirectory>false</includeBaseDirectory> + + <componentDescriptors> + <componentDescriptor>${project.basedir}/src/main/assembly/common-dependencies.xml</componentDescriptor> + </componentDescriptors> + + <dependencySets> + <!-- JAX-RS API --> + <dependencySet> + <useProjectArtifact>false</useProjectArtifact> + <useTransitiveDependencies>true</useTransitiveDependencies> + <outputDirectory>jaxrs-ri/api</outputDirectory> + <directoryMode>0755</directoryMode> + <fileMode>0644</fileMode> + <includes> + <include>javax.ws.rs:*</include> + </includes> + <excludes> + <exclude>javax.ws.rs:*:*:sources:*</exclude> + </excludes> + </dependencySet> + <!-- JAX-RS RI Binaries--> + <dependencySet> + <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping> + <useProjectArtifact>false</useProjectArtifact> + <useTransitiveDependencies>false</useTransitiveDependencies> + <outputDirectory>jaxrs-ri/lib</outputDirectory> + <directoryMode>0755</directoryMode> + <fileMode>0644</fileMode> + <includes> + <include>org.glassfish.jersey.*</include> + </includes> + <excludes> + <exclude>*:sources:*</exclude> + </excludes> + </dependencySet> + </dependencySets> + + <!-- TODO apidocs? --> +</assembly>
diff --git a/bundles/jaxrs-ri/src/main/assembly/assembly-src-licensee.xml b/bundles/jaxrs-ri/src/main/assembly/assembly-src-licensee.xml new file mode 100644 index 0000000..635b53c --- /dev/null +++ b/bundles/jaxrs-ri/src/main/assembly/assembly-src-licensee.xml
@@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>src</id> + + <formats> + <format>zip</format> + <format>tar.gz</format> + <format>tar.bz2</format> + </formats> + + <includeBaseDirectory>false</includeBaseDirectory> + + <componentDescriptors> + <componentDescriptor>${project.basedir}/src/main/assembly/common-dependencies.xml</componentDescriptor> + </componentDescriptors> + + <dependencySets> + <!-- JAX-RS RI Sources --> + <dependencySet> + <useProjectArtifact>false</useProjectArtifact> + <useTransitiveDependencies>false</useTransitiveDependencies> + <outputDirectory>jaxrs-ri/src</outputDirectory> + <directoryMode>0755</directoryMode> + <fileMode>0644</fileMode> + <unpack>true</unpack> + <unpackOptions> + <excludes> + <exclude>META-INF/MANIFEST.MF</exclude> + <exclude>META-INF/NOTICE.md</exclude> + </excludes> + </unpackOptions> + <includes> + <include>javax.ws.rs:*:*:sources:*</include> + <include>org.glassfish.jersey.*:*:*:sources:*</include> + </includes> + </dependencySet> + </dependencySets> + + <fileSets> + <fileSet> + <directory>${project.build.directory}/license</directory> + <outputDirectory>jaxrs-ri</outputDirectory> + </fileSet> + <fileSet> + <directory>${project.basedir}/src/main/resources</directory> + <outputDirectory>jaxrs-ri</outputDirectory> + </fileSet> + <fileSet> + <directory>${project.build.directory}/dependency/META-INF/services</directory> + <outputDirectory>jaxrs-ri/src/META-INF/services</outputDirectory> + </fileSet> + </fileSets> +</assembly>
diff --git a/bundles/jaxrs-ri/src/main/assembly/assembly-src.xml b/bundles/jaxrs-ri/src/main/assembly/assembly-src.xml new file mode 100644 index 0000000..1cd0035 --- /dev/null +++ b/bundles/jaxrs-ri/src/main/assembly/assembly-src.xml
@@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<assembly> + <id>src</id> + + <formats> + <format>zip</format> + <format>tar.gz</format> + <format>tar.bz2</format> + </formats> + + <includeBaseDirectory>false</includeBaseDirectory> + + <componentDescriptors> + <componentDescriptor>${project.basedir}/src/main/assembly/common-dependencies.xml</componentDescriptor> + </componentDescriptors> + + <dependencySets> + <!-- JAX-RS RI Sources --> + <dependencySet> + <useProjectArtifact>false</useProjectArtifact> + <useTransitiveDependencies>false</useTransitiveDependencies> + <outputDirectory>jaxrs-ri/src</outputDirectory> + <directoryMode>0755</directoryMode> + <fileMode>0644</fileMode> + <unpack>true</unpack> + <unpackOptions> + <excludes> + <exclude>META-INF/MANIFEST.MF</exclude> + <exclude>META-INF/NOTICE.md</exclude> + </excludes> + </unpackOptions> + <includes> + <include>javax.ws.rs:*:*:sources:*</include> + <include>org.glassfish.jersey.*:*:*:sources:*</include> + </includes> + </dependencySet> + </dependencySets> + + <fileSets> + <fileSet> + <directory>${project.basedir}/src/main/resources</directory> + <outputDirectory>jaxrs-ri</outputDirectory> + </fileSet> + <fileSet> + <directory>${project.build.directory}/dependency/META-INF/services</directory> + <outputDirectory>jaxrs-ri/src/META-INF/services</outputDirectory> + </fileSet> + </fileSets> +</assembly>
diff --git a/bundles/jaxrs-ri/src/main/assembly/common-dependencies.xml b/bundles/jaxrs-ri/src/main/assembly/common-dependencies.xml new file mode 100644 index 0000000..61e32e4 --- /dev/null +++ b/bundles/jaxrs-ri/src/main/assembly/common-dependencies.xml
@@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<component> + <dependencySets> + <!-- JAX-RS RI runtime-scoped dependencies --> + <dependencySet> + <useProjectArtifact>false</useProjectArtifact> + <useTransitiveDependencies>true</useTransitiveDependencies> + <outputDirectory>jaxrs-ri/ext</outputDirectory> + <directoryMode>0755</directoryMode> + <fileMode>0644</fileMode> + <excludes> + <exclude>javax.ws.rs:*</exclude> + <exclude>org.glassfish.jersey.*:*</exclude> + <!-- CDI API dependencies come from yasson, cdi is optional there --> + <exclude>javax.enterprise:cdi-api:jar:*</exclude> + <exclude>javax.el:el-api:jar:*</exclude> + <exclude>org.jboss.spec.javax.interceptor:jboss-interceptors-api_1.1_spec:jar:*</exclude> + <exclude>javax.annotation:jsr250-api:jar:*</exclude> + </excludes> + </dependencySet> + <!-- JAX-RS RI provided dependencies --> + <dependencySet> + <useProjectArtifact>false</useProjectArtifact> + <useTransitiveDependencies>true</useTransitiveDependencies> + <outputDirectory>jaxrs-ri/ext</outputDirectory> + <directoryMode>0755</directoryMode> + <fileMode>0644</fileMode> + <scope>provided</scope> + </dependencySet> + </dependencySets> + <files> + <file> + <source>../../LICENSE.md</source> + <destName>Jersey-LICENSE.md</destName> + <outputDirectory>jaxrs-ri</outputDirectory> + </file> + <file> + <source>../../third-party-license-readme.txt</source> + <outputDirectory>jaxrs-ri</outputDirectory> + </file> + <file> + <source>../../NOTICE.md</source> + <outputDirectory>jaxrs-ri</outputDirectory> + </file> + </files> +</component>
diff --git a/bundles/jaxrs-ri/src/main/resources/build.xml b/bundles/jaxrs-ri/src/main/resources/build.xml new file mode 100644 index 0000000..7f2fbdf --- /dev/null +++ b/bundles/jaxrs-ri/src/main/resources/build.xml
@@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<!-- *********************** JAX-RS RI build file ************************* --> +<project name="jax-rs-ri" default="build"> + <target name="build"> + <!-- compile --> + <mkdir dir="build"/> + <javac srcdir="src" destdir="build"> + <classpath> + <fileset dir="ext" includes="**/*.jar"/> + </classpath> + </javac> + </target> +</project>
diff --git a/bundles/pom.xml b/bundles/pom.xml new file mode 100644 index 0000000..3f4f0d4 --- /dev/null +++ b/bundles/pom.xml
@@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.bundles</groupId> + <artifactId>project</artifactId> + <packaging>pom</packaging> + <name>jersey-bundles</name> + + <description>Jersey bundles providers umbrella project module.</description> + + <modules> + <module>jaxrs-ri</module> + </modules> + + <profiles> + <profile> + <id>release</id> + <activation> + <activeByDefault>false</activeByDefault> + </activation> + <modules> + <module>apidocs</module> + <module>examples</module> + </modules> + </profile> + </profiles> + +</project>
diff --git a/connectors/apache-connector/pom.xml b/connectors/apache-connector/pom.xml new file mode 100644 index 0000000..7ac1ecd --- /dev/null +++ b/connectors/apache-connector/pom.xml
@@ -0,0 +1,89 @@ +<?xml version="1.0"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-apache-connector</artifactId> + <packaging>jar</packaging> + <name>jersey-connectors-apache</name> + + <description>Jersey Client Transport via Apache</description> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-grizzly2</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + </plugins> + </build> + +</project>
diff --git a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheClientProperties.java b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheClientProperties.java new file mode 100644 index 0000000..6f87627 --- /dev/null +++ b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheClientProperties.java
@@ -0,0 +1,161 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.util.Map; + +import org.glassfish.jersey.internal.util.PropertiesClass; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +/** + * Configuration options specific to the Client API that utilizes {@link ApacheConnectorProvider}. + * + * @author jorgeluisw@mac.com + * @author Paul Sandoz + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +@PropertiesClass +public final class ApacheClientProperties { + + /** + * The credential provider that should be used to retrieve + * credentials from a user. Credentials needed for proxy authentication + * are stored here as well. + * <p/> + * The value MUST be an instance of {@link org.apache.http.client.CredentialsProvider}. + * <p/> + * If the property is absent a default provider will be used. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String CREDENTIALS_PROVIDER = "jersey.config.apache.client.credentialsProvider"; + + /** + * A value of {@code false} indicates the client should handle cookies + * automatically using HttpClient's default cookie policy. A value + * of {@code true} will cause the client to ignore all cookies. + * <p/> + * The value MUST be an instance of {@link java.lang.Boolean}. + * <p/> + * The default value is {@code false}. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String DISABLE_COOKIES = "jersey.config.apache.client.handleCookies"; + + /** + * A value of {@code true} indicates that a client should send an + * authentication request even before the server gives a 401 + * response. + * <p> + * This property may only be set prior to constructing Apache connector using {@link ApacheConnectorProvider}. + * <p/> + * The value MUST be an instance of {@link java.lang.Boolean}. + * <p/> + * The default value is {@code false}. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String PREEMPTIVE_BASIC_AUTHENTICATION = "jersey.config.apache.client.preemptiveBasicAuthentication"; + + /** + * Connection Manager which will be used to create {@link org.apache.http.client.HttpClient}. + * <p/> + * The value MUST be an instance of {@link org.apache.http.conn.HttpClientConnectionManager}. + * <p/> + * If the property is absent a default Connection Manager will be used + * ({@link org.apache.http.impl.conn.BasicHttpClientConnectionManager}). + * If you want to use this client in multi-threaded environment, be sure you override default value with + * {@link org.apache.http.impl.conn.PoolingHttpClientConnectionManager} instance. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String CONNECTION_MANAGER = "jersey.config.apache.client.connectionManager"; + + /** + * A value of {@code true} indicates that configured connection manager should be shared + * among multiple Jersey {@link org.glassfish.jersey.client.ClientRuntime} instances. It means that closing + * a particular {@link org.glassfish.jersey.client.ClientRuntime} instance does not shut down the underlying + * connection manager automatically. In such case, the connection manager life-cycle + * should be fully managed by the application code. To release all allocated resources, + * caller code should especially ensure {@link org.apache.http.conn.HttpClientConnectionManager#shutdown()} gets + * invoked eventually. + * <p> + * This property may only be set prior to constructing Apache connector using {@link ApacheConnectorProvider}. + * <p/> + * The value MUST be an instance of {@link java.lang.Boolean}. + * <p/> + * The default value is {@code false}. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + * + * @since 2.18 + */ + public static final String CONNECTION_MANAGER_SHARED = "jersey.config.apache.client.connectionManagerShared"; + + /** + * Request configuration for the {@link org.apache.http.client.HttpClient}. + * Http parameters which will be used to create {@link org.apache.http.client.HttpClient}. + * <p/> + * The value MUST be an instance of {@link org.apache.http.client.config.RequestConfig}. + * <p/> + * If the property is absent default request configuration will be used. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + * + * @since 2.5 + */ + public static final String REQUEST_CONFIG = "jersey.config.apache.client.requestConfig"; + + /** + * HttpRequestRetryHandler which will be used to create {@link org.apache.http.client.HttpClient}. + * <p/> + * The value MUST be an instance of {@link org.apache.http.client.HttpRequestRetryHandler}. + * <p/> + * If the property is absent a default retry handler will be used + * ({@link org.apache.http.impl.client.DefaultHttpRequestRetryHandler}). + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String RETRY_HANDLER = "jersey.config.apache.client.retryHandler"; + + /** + * Get the value of the specified property. + * + * If the property is not set or the actual property value type is not compatible with the specified type, the method will + * return {@code null}. + * + * @param properties Map of properties to get the property value from. + * @param key Name of the property. + * @param type Type to retrieve the value as. + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, final String key, final Class<T> type) { + return PropertiesHelper.getValue(properties, key, type, null); + } + + /** + * Prevents instantiation. + */ + private ApacheClientProperties() { + throw new AssertionError("No instances allowed."); + } +}
diff --git a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java new file mode 100644 index 0000000..f5874dc --- /dev/null +++ b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java
@@ -0,0 +1,676 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.message.internal.HeaderUtils; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.message.internal.ReaderWriter; +import org.glassfish.jersey.message.internal.Statuses; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CookieStore; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.ConnectionConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.ManagedHttpClientConnection; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContexts; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.entity.ContentLengthStrategy; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.DefaultManagedHttpClientConnection; +import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.impl.io.ChunkedOutputStream; +import org.apache.http.io.SessionOutputBuffer; +import org.apache.http.util.TextUtils; +import org.apache.http.util.VersionInfo; + +/** + * A {@link Connector} that utilizes the Apache HTTP Client to send and receive + * HTTP request and responses. + * <p/> + * The following properties are only supported at construction of this class: + * <ul> + * <li>{@link ApacheClientProperties#CONNECTION_MANAGER}</li> + * <li>{@link ApacheClientProperties#REQUEST_CONFIG}</li> + * <li>{@link ApacheClientProperties#CREDENTIALS_PROVIDER}</li> + * <li>{@link ApacheClientProperties#DISABLE_COOKIES}</li> + * <li>{@link ClientProperties#PROXY_URI}</li> + * <li>{@link ClientProperties#PROXY_USERNAME}</li> + * <li>{@link ClientProperties#PROXY_PASSWORD}</li> + * <li>{@link ClientProperties#REQUEST_ENTITY_PROCESSING} - default value is {@link RequestEntityProcessing#CHUNKED}</li> + * <li>{@link ApacheClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li> + * <li>{@link ApacheClientProperties#RETRY_HANDLER}</li> + * </ul> + * <p> + * This connector uses {@link RequestEntityProcessing#CHUNKED chunked encoding} as a default setting. This can + * be overridden by the {@link ClientProperties#REQUEST_ENTITY_PROCESSING}. By default the + * {@link ClientProperties#CHUNKED_ENCODING_SIZE} property is only supported by using default connection manager. If custom + * connection manager needs to be used then chunked encoding size can be set by providing a custom + * {@link org.apache.http.HttpClientConnection} (via custom {@link org.apache.http.impl.conn.ManagedHttpClientConnectionFactory}) + * and overriding {@code createOutputStream} method. + * </p> + * <p> + * Using of authorization is dependent on the chunk encoding setting. If the entity + * buffering is enabled, the entity is buffered and authorization can be performed + * automatically in response to a 401 by sending the request again. When entity buffering + * is disabled (chunked encoding is used) then the property + * {@link org.glassfish.jersey.apache.connector.ApacheClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION} must + * be set to {@code true}. + * </p> + * <p> + * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an + * entity is not read from the response then + * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called + * after processing the response to release connection-based resources. + * </p> + * <p> + * Client operations are thread safe, the HTTP connection may + * be shared between different threads. + * </p> + * <p> + * If a response entity is obtained that is an instance of {@link Closeable} + * then the instance MUST be closed after processing the entity to release + * connection-based resources. + * </p> + * <p> + * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE. + * </p> + * + * @author jorgeluisw@mac.com + * @author Paul Sandoz + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Arul Dhesiaseelan (aruld at acm.org) + * @see ApacheClientProperties#CONNECTION_MANAGER + */ +@SuppressWarnings("deprecation") +class ApacheConnector implements Connector { + + private static final Logger LOGGER = Logger.getLogger(ApacheConnector.class.getName()); + + private static final VersionInfo vi; + private static final String release; + + static { + vi = VersionInfo.loadVersionInfo("org.apache.http.client", HttpClientBuilder.class.getClassLoader()); + release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE; + } + + private final CloseableHttpClient client; + private final CookieStore cookieStore; + private final boolean preemptiveBasicAuth; + private final RequestConfig requestConfig; + + /** + * Create the new Apache HTTP Client connector. + * + * @param client JAX-RS client instance for which the connector is being created. + * @param config client configuration. + */ + ApacheConnector(final Client client, final Configuration config) { + final Object connectionManager = config.getProperties().get(ApacheClientProperties.CONNECTION_MANAGER); + if (connectionManager != null) { + if (!(connectionManager instanceof HttpClientConnectionManager)) { + LOGGER.log( + Level.WARNING, + LocalizationMessages.IGNORING_VALUE_OF_PROPERTY( + ApacheClientProperties.CONNECTION_MANAGER, + connectionManager.getClass().getName(), + HttpClientConnectionManager.class.getName()) + ); + } + } + + Object reqConfig = config.getProperties().get(ApacheClientProperties.REQUEST_CONFIG); + if (reqConfig != null) { + if (!(reqConfig instanceof RequestConfig)) { + LOGGER.log( + Level.WARNING, + LocalizationMessages.IGNORING_VALUE_OF_PROPERTY( + ApacheClientProperties.REQUEST_CONFIG, + reqConfig.getClass().getName(), + RequestConfig.class.getName()) + ); + reqConfig = null; + } + } + + final SSLContext sslContext = client.getSslContext(); + final HttpClientBuilder clientBuilder = HttpClientBuilder.create(); + + clientBuilder.setConnectionManager(getConnectionManager(client, config, sslContext)); + clientBuilder.setConnectionManagerShared( + PropertiesHelper.getValue(config.getProperties(), ApacheClientProperties.CONNECTION_MANAGER_SHARED, false, null)); + clientBuilder.setSslcontext(sslContext); + + final RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + + final Object credentialsProvider = config.getProperty(ApacheClientProperties.CREDENTIALS_PROVIDER); + if (credentialsProvider != null && (credentialsProvider instanceof CredentialsProvider)) { + clientBuilder.setDefaultCredentialsProvider((CredentialsProvider) credentialsProvider); + } + + final Object retryHandler = config.getProperties().get(ApacheClientProperties.RETRY_HANDLER); + if (retryHandler != null && (retryHandler instanceof HttpRequestRetryHandler)) { + clientBuilder.setRetryHandler((HttpRequestRetryHandler) retryHandler); + } + + final Object proxyUri; + proxyUri = config.getProperty(ClientProperties.PROXY_URI); + if (proxyUri != null) { + final URI u = getProxyUri(proxyUri); + final HttpHost proxy = new HttpHost(u.getHost(), u.getPort(), u.getScheme()); + final String userName; + userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class); + if (userName != null) { + final String password; + password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class); + + if (password != null) { + final CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(u.getHost(), u.getPort()), + new UsernamePasswordCredentials(userName, password) + ); + clientBuilder.setDefaultCredentialsProvider(credsProvider); + } + } + clientBuilder.setProxy(proxy); + } + + final Boolean preemptiveBasicAuthProperty = (Boolean) config.getProperties() + .get(ApacheClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION); + this.preemptiveBasicAuth = (preemptiveBasicAuthProperty != null) ? preemptiveBasicAuthProperty : false; + + final boolean ignoreCookies = PropertiesHelper.isProperty(config.getProperties(), ApacheClientProperties.DISABLE_COOKIES); + + if (reqConfig != null) { + final RequestConfig.Builder reqConfigBuilder = RequestConfig.copy((RequestConfig) reqConfig); + if (ignoreCookies) { + reqConfigBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES); + } + requestConfig = reqConfigBuilder.build(); + } else { + if (ignoreCookies) { + requestConfigBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES); + } + requestConfig = requestConfigBuilder.build(); + } + + if (requestConfig.getCookieSpec() == null || !requestConfig.getCookieSpec().equals(CookieSpecs.IGNORE_COOKIES)) { + this.cookieStore = new BasicCookieStore(); + clientBuilder.setDefaultCookieStore(cookieStore); + } else { + this.cookieStore = null; + } + clientBuilder.setDefaultRequestConfig(requestConfig); + this.client = clientBuilder.build(); + } + + private HttpClientConnectionManager getConnectionManager(final Client client, + final Configuration config, + final SSLContext sslContext) { + final Object cmObject = config.getProperties().get(ApacheClientProperties.CONNECTION_MANAGER); + + // Connection manager from configuration. + if (cmObject != null) { + if (cmObject instanceof HttpClientConnectionManager) { + return (HttpClientConnectionManager) cmObject; + } else { + LOGGER.log( + Level.WARNING, + LocalizationMessages.IGNORING_VALUE_OF_PROPERTY( + ApacheClientProperties.CONNECTION_MANAGER, + cmObject.getClass().getName(), + HttpClientConnectionManager.class.getName()) + ); + } + } + + // Create custom connection manager. + return createConnectionManager( + client, + config, + sslContext, + false); + } + + private HttpClientConnectionManager createConnectionManager( + final Client client, + final Configuration config, + final SSLContext sslContext, + final boolean useSystemProperties) { + + final String[] supportedProtocols = useSystemProperties ? split( + System.getProperty("https.protocols")) : null; + final String[] supportedCipherSuites = useSystemProperties ? split( + System.getProperty("https.cipherSuites")) : null; + + HostnameVerifier hostnameVerifier = client.getHostnameVerifier(); + + final LayeredConnectionSocketFactory sslSocketFactory; + if (sslContext != null) { + sslSocketFactory = new SSLConnectionSocketFactory( + sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } else { + if (useSystemProperties) { + sslSocketFactory = new SSLConnectionSocketFactory( + (SSLSocketFactory) SSLSocketFactory.getDefault(), + supportedProtocols, supportedCipherSuites, hostnameVerifier); + } else { + sslSocketFactory = new SSLConnectionSocketFactory( + SSLContexts.createDefault(), + hostnameVerifier); + } + } + + final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", sslSocketFactory) + .build(); + + final Integer chunkSize = ClientProperties.getValue(config.getProperties(), + ClientProperties.CHUNKED_ENCODING_SIZE, ClientProperties.DEFAULT_CHUNK_SIZE, Integer.class); + + final PoolingHttpClientConnectionManager connectionManager = + new PoolingHttpClientConnectionManager(registry, new ConnectionFactory(chunkSize)); + + if (useSystemProperties) { + String s = System.getProperty("http.keepAlive", "true"); + if ("true".equalsIgnoreCase(s)) { + s = System.getProperty("http.maxConnections", "5"); + final int max = Integer.parseInt(s); + connectionManager.setDefaultMaxPerRoute(max); + connectionManager.setMaxTotal(2 * max); + } + } + + return connectionManager; + } + + private static String[] split(final String s) { + if (TextUtils.isBlank(s)) { + return null; + } + return s.split(" *, *"); + } + + /** + * Get the {@link HttpClient}. + * + * @return the {@link HttpClient}. + */ + @SuppressWarnings("UnusedDeclaration") + public HttpClient getHttpClient() { + return client; + } + + /** + * Get the {@link CookieStore}. + * + * @return the {@link CookieStore} instance or {@code null} when {@value ApacheClientProperties#DISABLE_COOKIES} set to + * {@code true}. + */ + public CookieStore getCookieStore() { + return cookieStore; + } + + private static URI getProxyUri(final Object proxy) { + if (proxy instanceof URI) { + return (URI) proxy; + } else if (proxy instanceof String) { + return URI.create((String) proxy); + } else { + throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI)); + } + } + + @Override + public ClientResponse apply(final ClientRequest clientRequest) throws ProcessingException { + final HttpUriRequest request = getUriHttpRequest(clientRequest); + final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(clientRequest.getHeaders(), request); + + try { + final CloseableHttpResponse response; + final HttpClientContext context = HttpClientContext.create(); + if (preemptiveBasicAuth) { + final AuthCache authCache = new BasicAuthCache(); + final BasicScheme basicScheme = new BasicScheme(); + authCache.put(getHost(request), basicScheme); + context.setAuthCache(authCache); + } + + // If a request-specific CredentialsProvider exists, use it instead of the default one + CredentialsProvider credentialsProvider = + clientRequest.resolveProperty(ApacheClientProperties.CREDENTIALS_PROVIDER, CredentialsProvider.class); + if (credentialsProvider != null) { + context.setCredentialsProvider(credentialsProvider); + } + + response = client.execute(getHost(request), request, context); + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(), this.getClass().getName()); + + final Response.StatusType status = response.getStatusLine().getReasonPhrase() == null + ? Statuses.from(response.getStatusLine().getStatusCode()) + : Statuses.from(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + + final ClientResponse responseContext = new ClientResponse(status, clientRequest); + final List<URI> redirectLocations = context.getRedirectLocations(); + if (redirectLocations != null && !redirectLocations.isEmpty()) { + responseContext.setResolvedRequestUri(redirectLocations.get(redirectLocations.size() - 1)); + } + + final Header[] respHeaders = response.getAllHeaders(); + final MultivaluedMap<String, String> headers = responseContext.getHeaders(); + for (final Header header : respHeaders) { + final String headerName = header.getName(); + List<String> list = headers.get(headerName); + if (list == null) { + list = new ArrayList<>(); + } + list.add(header.getValue()); + headers.put(headerName, list); + } + + final HttpEntity entity = response.getEntity(); + + if (entity != null) { + if (headers.get(HttpHeaders.CONTENT_LENGTH) == null) { + headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength())); + } + + final Header contentEncoding = entity.getContentEncoding(); + if (headers.get(HttpHeaders.CONTENT_ENCODING) == null && contentEncoding != null) { + headers.add(HttpHeaders.CONTENT_ENCODING, contentEncoding.getValue()); + } + } + + try { + responseContext.setEntityStream(new HttpClientResponseInputStream(getInputStream(response))); + } catch (final IOException e) { + LOGGER.log(Level.SEVERE, null, e); + } + + return responseContext; + } catch (final Exception e) { + throw new ProcessingException(e); + } + } + + @Override + public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) { + try { + ClientResponse response = apply(request); + callback.response(response); + return CompletableFuture.completedFuture(response); + } catch (Throwable t) { + callback.failure(t); + CompletableFuture<Object> future = new CompletableFuture<>(); + future.completeExceptionally(t); + return future; + } + } + + @Override + public String getName() { + return "Apache HttpClient " + release; + } + + @Override + public void close() { + try { + client.close(); + } catch (final IOException e) { + throw new ProcessingException(LocalizationMessages.FAILED_TO_STOP_CLIENT(), e); + } + } + + private HttpHost getHost(final HttpUriRequest request) { + return new HttpHost(request.getURI().getHost(), request.getURI().getPort(), request.getURI().getScheme()); + } + + private HttpUriRequest getUriHttpRequest(final ClientRequest clientRequest) { + final RequestConfig.Builder requestConfigBuilder = RequestConfig.copy(requestConfig); + + final int connectTimeout = clientRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, -1); + final int socketTimeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, -1); + + if (connectTimeout >= 0) { + requestConfigBuilder.setConnectTimeout(connectTimeout); + } + if (socketTimeout >= 0) { + requestConfigBuilder.setSocketTimeout(socketTimeout); + } + + final Boolean redirectsEnabled = + clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, requestConfig.isRedirectsEnabled()); + requestConfigBuilder.setRedirectsEnabled(redirectsEnabled); + + final Boolean bufferingEnabled = clientRequest.resolveProperty(ClientProperties.REQUEST_ENTITY_PROCESSING, + RequestEntityProcessing.class) == RequestEntityProcessing.BUFFERED; + final HttpEntity entity = getHttpEntity(clientRequest, bufferingEnabled); + + return RequestBuilder + .create(clientRequest.getMethod()) + .setUri(clientRequest.getUri()) + .setConfig(requestConfigBuilder.build()) + .setEntity(entity) + .build(); + } + + private HttpEntity getHttpEntity(final ClientRequest clientRequest, final boolean bufferingEnabled) { + final Object entity = clientRequest.getEntity(); + + if (entity == null) { + return null; + } + + final AbstractHttpEntity httpEntity = new AbstractHttpEntity() { + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public long getContentLength() { + return -1; + } + + @Override + public InputStream getContent() throws IOException, IllegalStateException { + if (bufferingEnabled) { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(512); + writeTo(buffer); + return new ByteArrayInputStream(buffer.toByteArray()); + } else { + return null; + } + } + + @Override + public void writeTo(final OutputStream outputStream) throws IOException { + clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { + @Override + public OutputStream getOutputStream(final int contentLength) throws IOException { + return outputStream; + } + }); + clientRequest.writeEntity(); + } + + @Override + public boolean isStreaming() { + return false; + } + }; + + if (bufferingEnabled) { + try { + return new BufferedHttpEntity(httpEntity); + } catch (final IOException e) { + throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e); + } + } else { + return httpEntity; + } + } + + private static Map<String, String> writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, + final HttpUriRequest request) { + final Map<String, String> stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers); + + for (final Map.Entry<String, String> e : stringHeaders.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + return stringHeaders; + } + + private static final class HttpClientResponseInputStream extends FilterInputStream { + + HttpClientResponseInputStream(final InputStream inputStream) throws IOException { + super(inputStream); + } + + @Override + public void close() throws IOException { + super.close(); + } + } + + private static InputStream getInputStream(final CloseableHttpResponse response) throws IOException { + + final InputStream inputStream; + + if (response.getEntity() == null) { + inputStream = new ByteArrayInputStream(new byte[0]); + } else { + final InputStream i = response.getEntity().getContent(); + if (i.markSupported()) { + inputStream = i; + } else { + inputStream = new BufferedInputStream(i, ReaderWriter.BUFFER_SIZE); + } + } + + return new FilterInputStream(inputStream) { + @Override + public void close() throws IOException { + response.close(); + super.close(); + } + }; + } + + private static class ConnectionFactory extends ManagedHttpClientConnectionFactory { + + private static final AtomicLong COUNTER = new AtomicLong(); + + private final int chunkSize; + + private ConnectionFactory(final int chunkSize) { + this.chunkSize = chunkSize; + } + + @Override + public ManagedHttpClientConnection create(final HttpRoute route, final ConnectionConfig config) { + final String id = "http-outgoing-" + Long.toString(COUNTER.getAndIncrement()); + + return new HttpClientConnection(id, config.getBufferSize(), chunkSize); + } + } + + private static class HttpClientConnection extends DefaultManagedHttpClientConnection { + + private final int chunkSize; + + private HttpClientConnection(final String id, final int buffersize, final int chunkSize) { + super(id, buffersize); + + this.chunkSize = chunkSize; + } + + @Override + protected OutputStream createOutputStream(final long len, final SessionOutputBuffer outbuffer) { + if (len == ContentLengthStrategy.CHUNKED) { + return new ChunkedOutputStream(chunkSize, outbuffer); + } + return super.createOutputStream(len, outbuffer); + } + } +}
diff --git a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnectorProvider.java b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnectorProvider.java new file mode 100644 index 0000000..1d988b0 --- /dev/null +++ b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnectorProvider.java
@@ -0,0 +1,150 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configurable; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.client.Initializable; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +import org.apache.http.client.CookieStore; +import org.apache.http.client.HttpClient; + +/** + * Connector provider for Jersey {@link Connector connectors} that utilize + * Apache HTTP Client to send and receive HTTP request and responses. + * <p> + * The following connector configuration properties are supported: + * <ul> + * <li>{@link ApacheClientProperties#CONNECTION_MANAGER}</li> + * <li>{@link ApacheClientProperties#REQUEST_CONFIG}</li> + * <li>{@link ApacheClientProperties#CREDENTIALS_PROVIDER}</li> + * <li>{@link ApacheClientProperties#DISABLE_COOKIES}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} + * - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}</li> + * <li>{@link ApacheClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li> + * <li>{@link ApacheClientProperties#RETRY_HANDLER}</li> + * </ul> + * </p> + * <p> + * Connector instances created via this connector provider use + * {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED chunked encoding} as a default setting. + * This can be overridden by the {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING}. + * By default the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property is only supported + * when using the default {@code org.apache.http.conn.HttpClientConnectionManager} instance. If custom + * connection manager is used, then chunked encoding size can be set by providing a custom + * {@code org.apache.http.HttpClientConnection} (via custom {@code org.apache.http.impl.conn.ManagedHttpClientConnectionFactory}) + * and overriding it's {@code createOutputStream} method. + * </p> + * <p> + * Use of authorization by the AHC-based connectors is dependent on the chunk encoding setting. + * If the entity buffering is enabled, the entity is buffered and authorization can be performed + * automatically in response to a 401 by sending the request again. When entity buffering + * is disabled (chunked encoding is used) then the property + * {@link org.glassfish.jersey.apache.connector.ApacheClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION} must + * be set to {@code true}. + * </p> + * <p> + * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an entity is not read from the response then + * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called after processing the response to release + * connection-based resources. + * </p> + * <p> + * If a response entity is obtained that is an instance of {@link java.io.Closeable} + * then the instance MUST be closed after processing the entity to release + * connection-based resources. + * <p/> + * <p> + * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE. + * <p/> + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author jorgeluisw at mac.com + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Paul Sandoz + * @author Maksim Mukosey (mmukosey at gmail.com) + * @since 2.5 + */ +public class ApacheConnectorProvider implements ConnectorProvider { + + @Override + public Connector getConnector(final Client client, final Configuration runtimeConfig) { + return new ApacheConnector(client, runtimeConfig); + } + + /** + * Retrieve the underlying Apache {@link HttpClient} instance from + * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget} + * configured to use {@code ApacheConnectorProvider}. + * + * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use + * {@code ApacheConnectorProvider}. + * @return underlying Apache {@code HttpClient} instance. + * + * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient} + * nor {@code JerseyWebTarget} instance or in case the component + * is not configured to use a {@code ApacheConnectorProvider}. + * @since 2.8 + */ + public static HttpClient getHttpClient(final Configurable<?> component) { + return getConnector(component).getHttpClient(); + } + + /** + * Retrieve the underlying Apache {@link CookieStore} instance from + * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget} + * configured to use {@code ApacheConnectorProvider}. + * + * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use + * {@code ApacheConnectorProvider}. + * @return underlying Apache {@code CookieStore} instance. + * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient} + * nor {@code JerseyWebTarget} instance or in case the component + * is not configured to use a {@code ApacheConnectorProvider}. + * @since 2.16 + */ + public static CookieStore getCookieStore(final Configurable<?> component) { + return getConnector(component).getCookieStore(); + } + + private static ApacheConnector getConnector(final Configurable<?> component) { + if (!(component instanceof Initializable)) { + throw new IllegalArgumentException( + LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName())); + } + + final Initializable<?> initializable = (Initializable<?>) component; + Connector connector = initializable.getConfiguration().getConnector(); + if (connector == null) { + initializable.preInitialize(); + connector = initializable.getConfiguration().getConnector(); + } + + if (connector instanceof ApacheConnector) { + return (ApacheConnector) connector; + } else { + throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED()); + } + } +}
diff --git a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/package-info.java b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/package-info.java new file mode 100644 index 0000000..fb1072d --- /dev/null +++ b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/package-info.java
@@ -0,0 +1,21 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on the + * Apache Http Client. + */ +package org.glassfish.jersey.apache.connector;
diff --git a/connectors/apache-connector/src/main/resources/org/glassfish/jersey/apache/connector/localization.properties b/connectors/apache-connector/src/main/resources/org/glassfish/jersey/apache/connector/localization.properties new file mode 100644 index 0000000..9977d76 --- /dev/null +++ b/connectors/apache-connector/src/main/resources/org/glassfish/jersey/apache/connector/localization.properties
@@ -0,0 +1,24 @@ +# +# Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +error.buffering.entity=Error buffering the entity. +failed.to.stop.client=Failed to stop the client. +# {0} - property name, e.g. jersey.config.client.httpclient.connectionManager; {1}, {2} - full class name +ignoring.value.of.property=Ignoring value of property "{0}" ("{1}") - not instance of "{2}". +# {0} - property name - jersey.config.client.httpclient.proxyUri +wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI. +invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget. +expected.connector.provider.not.used=The supplied component is not configured to use a ApacheConnectorProvider.
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AsyncTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AsyncTest.java new file mode 100644 index 0000000..3e3896f --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AsyncTest.java
@@ -0,0 +1,242 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.container.TimeoutHandler; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.hamcrest.Matchers; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Asynchronous connector test. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class AsyncTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName()); + private static final String PATH = "async"; + + /** + * Asynchronous test resource. + */ + @Path(PATH) + public static class AsyncResource { + /** + * Typical long-running operation duration. + */ + public static final long OPERATION_DURATION = 1000; + + /** + * Long-running asynchronous post. + * + * @param asyncResponse async response. + * @param id post request id (received as request payload). + */ + @POST + public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) { + LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName()); + new Thread(new Runnable() { + + @Override + public void run() { + String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep() + try { + Thread.sleep(OPERATION_DURATION); + return "DONE-" + id; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED-" + id; + } finally { + LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName()); + } + } + }, "async-post-runner-" + id).start(); + } + + /** + * Long-running async get request that times out. + * + * @param asyncResponse async response. + */ + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName()); + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + } + }); + asyncResponse.setTimeout(1, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // very expensive operation that typically finishes within 1 second but can take up to 5 seconds, + // simulated using sleep() + try { + Thread.sleep(5 * OPERATION_DURATION); + return "DONE"; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED"; + } finally { + LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName()); + } + } + }).start(); + } + + } + + @Override + protected Application configure() { + return new ResourceConfig(AsyncResource.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + config.connectorProvider(new ApacheConnectorProvider()); + } + + /** + * Test asynchronous POST. + * + * Send 3 async POST requests and wait to receive the responses. Check the response content and + * assert that the operation did not take more than twice as long as a single long operation duration + * (this ensures async request execution). + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncPost() throws Exception { + final long tic = System.currentTimeMillis(); + + // Submit requests asynchronously. + final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1")); + final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2")); + final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3")); + // get() waits for the response + + // workaround for AHC default connection manager limitation of + // only 2 open connections per host that may intermittently block + // the test + final CountDownLatch latch = new CountDownLatch(3); + ExecutorService executor = Executors.newFixedThreadPool(3); + + final Future<String> r1 = executor.submit(new Callable<String>() { + @Override + public String call() throws Exception { + try { + return rf1.get().readEntity(String.class); + } finally { + latch.countDown(); + } + } + }); + final Future<String> r2 = executor.submit(new Callable<String>() { + @Override + public String call() throws Exception { + try { + return rf2.get().readEntity(String.class); + } finally { + latch.countDown(); + } + } + }); + final Future<String> r3 = executor.submit(new Callable<String>() { + @Override + public String call() throws Exception { + try { + return rf3.get().readEntity(String.class); + } finally { + latch.countDown(); + } + } + }); + + assertTrue("Waiting for results has timed out.", latch.await(5 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS)); + final long toc = System.currentTimeMillis(); + + assertEquals("DONE-1", r1.get()); + assertEquals("DONE-2", r2.get()); + assertEquals("DONE-3", r3.get()); + + final int asyncTimeoutMultiplier = getAsyncTimeoutMultiplier(); + LOGGER.info("Using async timeout multiplier: " + asyncTimeoutMultiplier); + assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(4 * AsyncResource.OPERATION_DURATION + * asyncTimeoutMultiplier)); + + } + + /** + * Test accessing an operation that times out on the server. + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncGetWithTimeout() throws Exception { + final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java new file mode 100644 index 0000000..6a98130 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java
@@ -0,0 +1,351 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import javax.inject.Singleton; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class AuthTest extends JerseyTest { + + @Override + protected Application configure() { + return new ResourceConfig(PreemptiveAuthResource.class, AuthResource.class); + } + + @Path("/") + public static class PreemptiveAuthResource { + + @GET + public String get(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + assertNotNull(value); + return "GET"; + } + + @POST + public String post(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + assertNotNull(value); + return e; + } + } + + @Test + public void testPreemptiveAuth() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider) + .property(ApacheClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + assertEquals("GET", r.request().get(String.class)); + } + + @Test + public void testPreemptiveAuthPost() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider) + .property(ApacheClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + } + + @Path("/test") + @Singleton + public static class AuthResource { + + int requestCount = 0; + + @GET + public String get(@Context HttpHeaders h) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + + return "GET"; + } + + @GET + @Path("filter") + public String getFilter(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return "GET"; + } + + @POST + public String post(@Context HttpHeaders h, String e) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + + return e; + } + + @POST + @Path("filter") + public String postFilter(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return e; + } + + @DELETE + public void delete(@Context HttpHeaders h) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + } + + @DELETE + @Path("filter") + public void deleteFilter(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + } + + @DELETE + @Path("filter/withEntity") + public String deleteFilterWithEntity(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return e; + } + } + + @Test + public void testAuthGet() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()).path("test"); + + assertEquals("GET", r.request().get(String.class)); + } + + @Test + public void testAuthGetWithRequestCredentialsProvider() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()).path("test"); + + assertEquals("GET", + r.request() + .property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider) + .get(String.class)); + } + + @Test + public void testAuthGetWithClientFilter() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + client.register(HttpAuthenticationFeature.basic("name", "password")); + WebTarget r = client.target(getBaseUri()).path("test/filter"); + + assertEquals("GET", r.request().get(String.class)); + } + + @Test + @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?" + + " Allow repeatable write in jersey?") + public void testAuthPost() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()).path("test"); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + } + + @Test + public void testAuthPostWithClientFilter() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + client.register(HttpAuthenticationFeature.basic("name", "password")); + WebTarget r = client.target(getBaseUri()).path("test/filter"); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + } + + @Test + public void testAuthDelete() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()).path("test"); + + Response response = r.request().delete(); + assertEquals(response.getStatus(), 204); + } + + @Test + public void testAuthDeleteWithClientFilter() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + client.register(HttpAuthenticationFeature.basic("name", "password")); + WebTarget r = client.target(getBaseUri()).path("test/filter"); + + Response response = r.request().delete(); + assertEquals(204, response.getStatus()); + } + + @Test + public void testAuthInteractiveGet() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()).path("test"); + + assertEquals("GET", r.request().get(String.class)); + } + + @Test + @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?" + + " Allow repeatable write in jersey?") + public void testAuthInteractivePost() { + CredentialsProvider credentialsProvider = new org.apache.http.impl.client.BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("name", "password") + ); + + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CREDENTIALS_PROVIDER, credentialsProvider); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()).path("test"); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/CookieTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/CookieTest.java new file mode 100644 index 0000000..c387fce --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/CookieTest.java
@@ -0,0 +1,111 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class CookieTest extends JerseyTest { + + @Path("/") + public static class CookieResource { + + @GET + public Response get(@Context HttpHeaders h) { + Cookie c = h.getCookies().get("name"); + String e = (c == null) ? "NO-COOKIE" : c.getValue(); + return Response.ok(e) + .cookie(new NewCookie("name", "value")).build(); + } + } + + @Override + protected Application configure() { + return new ResourceConfig(CookieResource.class); + } + + @Test + public void testCookieResource() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("value", r.request().get(String.class)); + } + + @Test + public void testDisabledCookies() { + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.DISABLE_COOKIES, true); + cc.connectorProvider(new ApacheConnectorProvider()); + JerseyClient client = JerseyClientBuilder.createClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("NO-COOKIE", r.request().get(String.class)); + + final ApacheConnector connector = (ApacheConnector) client.getConfiguration().getConnector(); + if (connector.getCookieStore() != null) { + assertTrue(connector.getCookieStore().getCookies().isEmpty()); + } else { + assertNull(connector.getCookieStore()); + } + } + + @Test + public void testCookies() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + JerseyClient client = JerseyClientBuilder.createClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("value", r.request().get(String.class)); + + final ApacheConnector connector = (ApacheConnector) client.getConfiguration().getConnector(); + assertNotNull(connector.getCookieStore().getCookies()); + assertEquals(1, connector.getCookieStore().getCookies().size()); + assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue()); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/CustomLoggingFilter.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/CustomLoggingFilter.java new file mode 100644 index 0000000..d6c4259 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/CustomLoggingFilter.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; + +import static org.junit.Assert.assertEquals; + +/** + * Custom logging filter. + * + * @author Santiago Pericas-Geertsen (santiago.pericasgeertsen at oracle.com) + */ +public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter, + ClientRequestFilter, ClientResponseFilter { + + static int preFilterCalled = 0; + static int postFilterCalled = 0; + + @Override + public void filter(ClientRequestContext context) throws IOException { + System.out.println("CustomLoggingFilter.preFilter called"); + assertEquals(context.getConfiguration().getProperty("foo"), "bar"); + preFilterCalled++; + } + + @Override + public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException { + System.out.println("CustomLoggingFilter.postFilter called"); + assertEquals(context.getConfiguration().getProperty("foo"), "bar"); + postFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context) throws IOException { + System.out.println("CustomLoggingFilter.preFilter called"); + assertEquals(context.getProperty("foo"), "bar"); + preFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException { + System.out.println("CustomLoggingFilter.postFilter called"); + assertEquals(context.getProperty("foo"), "bar"); + postFilterCalled++; + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/DisableContentEncodingTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/DisableContentEncodingTest.java new file mode 100644 index 0000000..2fd7048 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/DisableContentEncodingTest.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.message.GZipEncoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.apache.http.client.config.RequestConfig; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Ondrej Kosatka (ondrej.kosatka at oracle.com) + */ +public class DisableContentEncodingTest extends JerseyTest { + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class); + } + + @Path("/") + public static class Resource { + + @GET + public String get(@HeaderParam("Accept-Encoding") String enc) { + return enc; + } + } + + @Test + public void testDisabledByRequestConfig() { + ClientConfig cc = new ClientConfig(GZipEncoder.class); + final RequestConfig requestConfig = RequestConfig.custom().setContentCompressionEnabled(false).build(); + cc.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()); + + String enc = r.request().get().readEntity(String.class); + assertEquals("", enc); + } + + @Test + public void testEnabledByRequestConfig() { + ClientConfig cc = new ClientConfig(GZipEncoder.class); + final RequestConfig requestConfig = RequestConfig.custom().setContentCompressionEnabled(true).build(); + cc.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()); + + String enc = r.request().get().readEntity(String.class); + assertEquals("gzip,deflate", enc); + } + + @Test + public void testDefaultEncoding() { + ClientConfig cc = new ClientConfig(GZipEncoder.class); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()); + + String enc = r.request().get().readEntity(String.class); + assertEquals("gzip,deflate", enc); + } + + @Test + public void testDefaultEncodingOverridden() { + ClientConfig cc = new ClientConfig(GZipEncoder.class); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()); + + String enc = r.request().acceptEncoding("gzip").get().readEntity(String.class); + assertEquals("gzip", enc); + } + +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/FollowRedirectsTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/FollowRedirectsTest.java new file mode 100644 index 0000000..267fd5d --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/FollowRedirectsTest.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Apache connector follow redirect tests. + * + * @author Martin Matula + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class FollowRedirectsTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName()); + + @Path("/test") + public static class RedirectResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("redirect") + public Response redirect() { + return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(RedirectResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + private static class RedirectTestFilter implements ClientResponseFilter { + public static final String RESOLVED_URI_HEADER = "resolved-uri"; + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (responseContext instanceof ClientResponse) { + ClientResponse clientResponse = (ClientResponse) responseContext; + responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString()); + } + } + } + + @Test + public void testDoFollow() { + Response r = target("test/redirect").register(RedirectTestFilter.class).request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + assertEquals( + UriBuilder.fromUri(getBaseUri()).path(RedirectResource.class).build().toString(), + r.getHeaderString(RedirectTestFilter.RESOLVED_URI_HEADER)); + } + + @Test + public void testDontFollow() { + WebTarget t = target("test/redirect"); + t.property(ClientProperties.FOLLOW_REDIRECTS, false); + assertEquals(303, t.request().get().getStatus()); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/GZIPContentEncodingTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/GZIPContentEncodingTest.java new file mode 100644 index 0000000..fb2503e --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,94 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.util.Arrays; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.message.GZipEncoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class GZIPContentEncodingTest extends JerseyTest { + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class); + } + + @Path("/") + public static class Resource { + + @POST + public byte[] post(byte[] content) { + return content; + } + } + + @Test + public void testPost() { + ClientConfig cc = new ClientConfig(GZipEncoder.class); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = client.target(getBaseUri()); + + byte[] content = new byte[1024 * 1024]; + assertTrue(Arrays.equals(content, + r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class))); + + Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + ClientConfig cc = new ClientConfig(GZipEncoder.class); + cc.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + + byte[] content = new byte[1024 * 1024]; + assertTrue(Arrays.equals(content, + r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class))); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + } + +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HelloWorldTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HelloWorldTest.java new file mode 100644 index 0000000..9c3bfd6 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HelloWorldTest.java
@@ -0,0 +1,614 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import javax.net.ssl.SSLSession; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.apache.http.HttpConnectionMetrics; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.ClientConnectionRequest; +import org.apache.http.conn.ConnectionPoolTimeoutException; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.ManagedClientConnection; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.impl.conn.BasicClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HttpContext; +import org.junit.Assert; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class HelloWorldTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName()); + private static final String ROOT_PATH = "helloworld"; + + @Path("helloworld") + public static class HelloWorldResource { + + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + + @GET + @Produces("text/plain") + @Path("error") + public Response getError() { + return Response.serverError().entity("Error.").build(); + } + + @GET + @Produces("text/plain") + @Path("error2") + public Response getError2() { + return Response.serverError().entity("Error2.").build(); + } + + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HelloWorldResource.class); + config.register(new LoggingFeature(LOGGER, Level.INFO, LoggingFeature.Verbosity.PAYLOAD_ANY, + LoggingFeature.DEFAULT_MAX_ENTITY_SIZE)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + @Test + public void testConnection() { + Response response = target().path(ROOT_PATH).request("text/plain").get(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testClientStringResponse() { + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + } + + @Test + public void testConnectionPoolSharingEnabled() throws Exception { + _testConnectionPoolSharing(true); + } + + @Test + public void testConnectionPoolSharingDisabled() throws Exception { + _testConnectionPoolSharing(false); + } + + public void _testConnectionPoolSharing(final boolean sharingEnabled) throws Exception { + + final HttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + + final ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager); + cc.property(ApacheClientProperties.CONNECTION_MANAGER_SHARED, sharingEnabled); + cc.connectorProvider(new ApacheConnectorProvider()); + + final Client clientOne = ClientBuilder.newClient(cc); + WebTarget target = clientOne.target(getBaseUri()).path(ROOT_PATH); + target.request().get(); + clientOne.close(); + + final boolean exceptionExpected = !sharingEnabled; + + final Client clientTwo = ClientBuilder.newClient(cc); + target = clientTwo.target(getBaseUri()).path(ROOT_PATH); + try { + target.request().get(); + if (exceptionExpected) { + Assert.fail("Exception expected"); + } + } catch (Exception e) { + if (!exceptionExpected) { + Assert.fail("Exception not expected"); + } + } finally { + clientTwo.close(); + } + + if (sharingEnabled) { + connectionManager.shutdown(); + } + } + + @Test + public void testAsyncClientRequests() throws InterruptedException { + HttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + ClientConfig cc = new ClientConfig(); + cc.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager); + cc.connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget target = client.target(getBaseUri()); + final int REQUESTS = 20; + final CountDownLatch latch = new CountDownLatch(REQUESTS); + final long tic = System.currentTimeMillis(); + final Map<Integer, String> results = new ConcurrentHashMap<Integer, String>(); + for (int i = 0; i < REQUESTS; i++) { + final int id = i; + target.path(ROOT_PATH).request().async().get(new InvocationCallback<Response>() { + @Override + public void completed(Response response) { + try { + final String result = response.readEntity(String.class); + results.put(id, result); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(Throwable error) { + Logger.getLogger(HelloWorldTest.class.getName()).log(Level.SEVERE, "Failed on throwable", error); + results.put(id, "error: " + error.getMessage()); + latch.countDown(); + } + }); + } + assertTrue(latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS)); + final long toc = System.currentTimeMillis(); + Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic)); + + StringBuilder resultInfo = new StringBuilder("Results:\n"); + for (int i = 0; i < REQUESTS; i++) { + String result = results.get(i); + resultInfo.append(i).append(": ").append(result).append('\n'); + } + Logger.getLogger(HelloWorldTest.class.getName()).info(resultInfo.toString()); + + for (int i = 0; i < REQUESTS; i++) { + String result = results.get(i); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, result); + } + } + + @Test + public void testHead() { + Response response = target().path(ROOT_PATH).request().head(); + assertEquals(200, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + } + + @Test + public void testFooBarOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals("foo/bar", response.getMediaType().toString()); + assertEquals(0, response.getLength()); + } + + @Test + public void testTextPlainOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + final String responseBody = response.readEntity(String.class); + _checkAllowContent(responseBody); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testMissingResourceNotFound() { + Response response; + + response = target().path(ROOT_PATH + "arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + + response = target().path(ROOT_PATH).path("arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + } + + @Test + public void testLoggingFilterClientClass() { + Client client = client(); + client.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterClientInstance() { + Client client = client(); + client.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetClass() { + WebTarget target = target().path(ROOT_PATH); + target.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetInstance() { + WebTarget target = target().path(ROOT_PATH); + target.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testConfigurationUpdate() { + Client client1 = client(); + client1.register(CustomLoggingFilter.class).property("foo", "bar"); + + Client client = ClientBuilder.newClient(client1.getConfiguration()); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = client.target(getBaseUri()).path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + /** + * JERSEY-2157 reproducer. + * <p> + * The test ensures that entities of the error responses which cause + * WebApplicationException being thrown by a JAX-RS client are buffered + * and that the underlying input connections are automatically released + * in such case. + */ + @Test + public void testConnectionClosingOnExceptionsForErrorResponses() { + final BasicClientConnectionManager cm = new BasicClientConnectionManager(); + final AtomicInteger connectionCounter = new AtomicInteger(0); + + final ClientConfig config = new ClientConfig().property(ApacheClientProperties.CONNECTION_MANAGER, + new ClientConnectionManager() { + @Override + public SchemeRegistry getSchemeRegistry() { + return cm.getSchemeRegistry(); + } + + @Override + public ClientConnectionRequest requestConnection(final HttpRoute route, final Object state) { + connectionCounter.incrementAndGet(); + + final ClientConnectionRequest wrappedRequest = cm.requestConnection(route, state); + + /** + * To explain the following long piece of code: + * + * All the code does is to just create a wrapper implementations + * for the AHC connection management interfaces. + * + * The only really important piece of code is the + * {@link org.apache.http.conn.ManagedClientConnection#releaseConnection()} implementation, + * where the connectionCounter is decremented when a managed connection instance + * is released by AHC runtime. In our test, this is expected to happen + * as soon as the exception is created for an error response + * (as the error response entity gets buffered in + * {@link org.glassfish.jersey.client.JerseyInvocation#convertToException(javax.ws.rs.core.Response)}). + */ + return new ClientConnectionRequest() { + @Override + public ManagedClientConnection getConnection(long timeout, TimeUnit tunit) + throws InterruptedException, ConnectionPoolTimeoutException { + + final ManagedClientConnection wrappedConnection = wrappedRequest.getConnection(timeout, tunit); + + return new ManagedClientConnection() { + @Override + public boolean isSecure() { + return wrappedConnection.isSecure(); + } + + @Override + public HttpRoute getRoute() { + return wrappedConnection.getRoute(); + } + + @Override + public SSLSession getSSLSession() { + return wrappedConnection.getSSLSession(); + } + + @Override + public void open(HttpRoute route, HttpContext context, HttpParams params) throws IOException { + wrappedConnection.open(route, context, params); + } + + @Override + public void tunnelTarget(boolean secure, HttpParams params) throws IOException { + wrappedConnection.tunnelTarget(secure, params); + } + + @Override + public void tunnelProxy(HttpHost next, boolean secure, HttpParams params) throws IOException { + wrappedConnection.tunnelProxy(next, secure, params); + } + + @Override + public void layerProtocol(HttpContext context, HttpParams params) throws IOException { + wrappedConnection.layerProtocol(context, params); + } + + @Override + public void markReusable() { + wrappedConnection.markReusable(); + } + + @Override + public void unmarkReusable() { + wrappedConnection.unmarkReusable(); + } + + @Override + public boolean isMarkedReusable() { + return wrappedConnection.isMarkedReusable(); + } + + @Override + public void setState(Object state) { + wrappedConnection.setState(state); + } + + @Override + public Object getState() { + return wrappedConnection.getState(); + } + + @Override + public void setIdleDuration(long duration, TimeUnit unit) { + wrappedConnection.setIdleDuration(duration, unit); + } + + @Override + public boolean isResponseAvailable(int timeout) throws IOException { + return wrappedConnection.isResponseAvailable(timeout); + } + + @Override + public void sendRequestHeader(HttpRequest request) throws HttpException, IOException { + wrappedConnection.sendRequestHeader(request); + } + + @Override + public void sendRequestEntity(HttpEntityEnclosingRequest request) + throws HttpException, IOException { + wrappedConnection.sendRequestEntity(request); + } + + @Override + public HttpResponse receiveResponseHeader() throws HttpException, IOException { + return wrappedConnection.receiveResponseHeader(); + } + + @Override + public void receiveResponseEntity(HttpResponse response) throws HttpException, IOException { + wrappedConnection.receiveResponseEntity(response); + } + + @Override + public void flush() throws IOException { + wrappedConnection.flush(); + } + + @Override + public void close() throws IOException { + wrappedConnection.close(); + } + + @Override + public boolean isOpen() { + return wrappedConnection.isOpen(); + } + + @Override + public boolean isStale() { + return wrappedConnection.isStale(); + } + + @Override + public void setSocketTimeout(int timeout) { + wrappedConnection.setSocketTimeout(timeout); + } + + @Override + public int getSocketTimeout() { + return wrappedConnection.getSocketTimeout(); + } + + @Override + public void shutdown() throws IOException { + wrappedConnection.shutdown(); + } + + @Override + public HttpConnectionMetrics getMetrics() { + return wrappedConnection.getMetrics(); + } + + @Override + public InetAddress getLocalAddress() { + return wrappedConnection.getLocalAddress(); + } + + @Override + public int getLocalPort() { + return wrappedConnection.getLocalPort(); + } + + @Override + public InetAddress getRemoteAddress() { + return wrappedConnection.getRemoteAddress(); + } + + @Override + public int getRemotePort() { + return wrappedConnection.getRemotePort(); + } + + @Override + public void releaseConnection() throws IOException { + connectionCounter.decrementAndGet(); + wrappedConnection.releaseConnection(); + } + + @Override + public void abortConnection() throws IOException { + wrappedConnection.abortConnection(); + } + + @Override + public String getId() { + return wrappedConnection.getId(); + } + + @Override + public void bind(Socket socket) throws IOException { + wrappedConnection.bind(socket); + } + + @Override + public Socket getSocket() { + return wrappedConnection.getSocket(); + } + }; + } + + @Override + public void abortRequest() { + wrappedRequest.abortRequest(); + } + }; + } + + @Override + public void releaseConnection(ManagedClientConnection conn, long keepalive, TimeUnit tunit) { + cm.releaseConnection(conn, keepalive, tunit); + } + + @Override + public void closeExpiredConnections() { + cm.closeExpiredConnections(); + } + + @Override + public void closeIdleConnections(long idletime, TimeUnit tunit) { + cm.closeIdleConnections(idletime, tunit); + } + + @Override + public void shutdown() { + cm.shutdown(); + } + }); + config.connectorProvider(new ApacheConnectorProvider()); + + final Client client = ClientBuilder.newClient(config); + final WebTarget rootTarget = client.target(getBaseUri()).path(ROOT_PATH); + + // Test that connection is getting closed properly for error responses. + try { + final String response = rootTarget.path("error").request().get(String.class); + fail("Exception expected. Received: " + response); + } catch (InternalServerErrorException isee) { + // do nothing - connection should be closed properly by now + } + + // Fail if the previous connection has not been closed automatically. + assertEquals(0, connectionCounter.get()); + + try { + final String response = rootTarget.path("error2").request().get(String.class); + fail("Exception expected. Received: " + response); + } catch (InternalServerErrorException isee) { + assertEquals("Received unexpected data.", "Error2.", isee.getResponse().readEntity(String.class)); + // Test buffering: + // second read would fail if entity was not buffered + assertEquals("Unexpected data in the entity buffer.", "Error2.", isee.getResponse().readEntity(String.class)); + } + + assertEquals(0, connectionCounter.get()); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpHeadersTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpHeadersTest.java new file mode 100644 index 0000000..5302bef --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpHeadersTest.java
@@ -0,0 +1,133 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.logging.Logger; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class HttpHeadersTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + if (transferEncoding == null || !transferEncoding.equals("chunked")) { + assertEquals("writer", xWriter); + } + return entity; + } + } + + @Provider + @Produces("text/plain") + public static class HeaderWriter implements MessageBodyWriter<String> { + + public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == String.class; + } + + public long getSize(String t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + public void writeTo(String t, + Class<?> type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap<String, Object> httpHeaders, + OutputStream entityStream) throws IOException, WebApplicationException { + httpHeaders.add("X-WRITER", "writer"); + entityStream.write(t.getBytes()); + } + } + + @Override + protected Application configure() { + enable(TestProperties.LOG_TRAFFIC); + enable(TestProperties.DUMP_ENTITY); + + ResourceConfig config = new ResourceConfig(HttpMethodResource.class, HeaderWriter.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.READ_TIMEOUT, 1000).connectorProvider(new ApacheConnectorProvider()); + } + + @Test + public void testPost() { + WebTarget r = target("test"); + + Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST")); + assertEquals(200, cr.getStatus()); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + WebTarget r = target("test"); + + Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST")); + assertEquals(200, cr.getStatus()); + assertTrue(cr.hasEntity()); + cr.close(); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpMethodTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpMethodTest.java new file mode 100644 index 0000000..3edabeb --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpMethodTest.java
@@ -0,0 +1,302 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class HttpMethodTest extends JerseyTest { + + @Override + protected Application configure() { + return new ResourceConfig(HttpMethodResource.class, ErrorResource.class); + } + + protected Client createClient() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + return ClientBuilder.newClient(cc); + } + + protected Client createPoolingClient() { + ClientConfig cc = new ClientConfig(); + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(100); + connectionManager.setDefaultMaxPerRoute(100); + cc.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager); + cc.connectorProvider(new ApacheConnectorProvider()); + return ClientBuilder.newClient(cc); + } + + private WebTarget getWebTarget(final Client client) { + return client.target(getBaseUri()).path("test"); + } + + private WebTarget getWebTarget() { + return getWebTarget(createClient()); + } + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @HttpMethod("PATCH") + public @interface PATCH { + } + + @Path("/test") + public static class HttpMethodResource { + @GET + public String get() { + return "GET"; + } + + @POST + public String post(String entity) { + return entity; + } + + @PUT + public String put(String entity) { + return entity; + } + + @DELETE + public String delete() { + return "DELETE"; + } + + @DELETE + @Path("withentity") + public String delete(String entity) { + return entity; + } + + @POST + @Path("noproduce") + public void postNoProduce(String entity) { + } + + @POST + @Path("noconsumeproduce") + public void postNoConsumeProduce() { + } + + @PATCH + public String patch(String entity) { + return entity; + } + } + + @Test + public void testHead() { + WebTarget r = getWebTarget(); + Response cr = r.request().head(); + assertFalse(cr.hasEntity()); + } + + @Test + public void testOptions() { + WebTarget r = getWebTarget(); + Response cr = r.request().options(); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testGet() { + WebTarget r = getWebTarget(); + assertEquals("GET", r.request().get(String.class)); + + Response cr = r.request().get(); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPost() { + WebTarget r = getWebTarget(); + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + ClientConfig cc = new ClientConfig() + .property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024) + .connectorProvider(new ApacheConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = getWebTarget(client); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostVoid() { + WebTarget r = getWebTarget(createPoolingClient()); + + for (int i = 0; i < 100; i++) { + r.request().post(Entity.text("POST")); + } + } + + @Test + public void testPostNoProduce() { + WebTarget r = getWebTarget(); + assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus()); + + Response cr = r.path("noproduce").request().post(Entity.text("POST")); + assertFalse(cr.hasEntity()); + cr.close(); + } + + + @Test + public void testPostNoConsumeProduce() { + WebTarget r = getWebTarget(); + assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus()); + + Response cr = r.path("noconsumeproduce").request().post(Entity.text("POST")); + assertFalse(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPut() { + WebTarget r = getWebTarget(); + assertEquals("PUT", r.request().put(Entity.text("PUT"), String.class)); + + Response cr = r.request().put(Entity.text("PUT")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testDelete() { + WebTarget r = getWebTarget(); + assertEquals("DELETE", r.request().delete(String.class)); + + Response cr = r.request().delete(); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPatch() { + WebTarget r = getWebTarget(); + assertEquals("PATCH", r.request().method("PATCH", Entity.text("PATCH"), String.class)); + + Response cr = r.request().method("PATCH", Entity.text("PATCH")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testAll() { + WebTarget r = getWebTarget(); + + assertEquals("GET", r.request().get(String.class)); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + + assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus()); + + assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus()); + + assertEquals("PUT", r.request().post(Entity.text("PUT"), String.class)); + + assertEquals("DELETE", r.request().delete(String.class)); + } + + + @Path("/error") + public static class ErrorResource { + @POST + public Response post(String entity) { + return Response.serverError().build(); + } + + @Path("entity") + @POST + public Response postWithEntity(String entity) { + return Response.serverError().entity("error").build(); + } + } + + @Test + public void testPostError() { + WebTarget r = createClient().target(getBaseUri()).path("error"); + + for (int i = 0; i < 100; i++) { + try { + final Response post = r.request().post(Entity.text("POST")); + post.close(); + } catch (ClientErrorException ex) { + } + } + } + + @Test + public void testPostErrorWithEntity() { + WebTarget r = createPoolingClient().target(getBaseUri()).path("error/entity"); + + for (int i = 0; i < 100; i++) { + try { + r.request().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + String s = ex.getResponse().readEntity(String.class); + assertEquals("error", s); + } + } + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpMethodWithClientFilterTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpMethodWithClientFilterTest.java new file mode 100644 index 0000000..b71fb1d --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/HttpMethodWithClientFilterTest.java
@@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class HttpMethodWithClientFilterTest extends HttpMethodTest { + + @Override + protected Client createClient() { + ClientConfig cc = new ClientConfig() + .register(LoggingFeature.class) + .connectorProvider(new ApacheConnectorProvider()); + return ClientBuilder.newClient(cc); + } + +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/LargeDataTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/LargeDataTest.java new file mode 100644 index 0000000..5483203 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/LargeDataTest.java
@@ -0,0 +1,156 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.StreamingOutput; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Assert; +import org.junit.Test; + +/** + * The LargeDataTest reproduces a problem when bytes of large data sent are incorrectly sent. + * As a result, the request body is different than what was sent by the client. + * <p> + * In order to be able to inspect the request body, the generated data is a sequence of numbers + * delimited with new lines. Such as + * <pre><code> + * 1 + * 2 + * 3 + * + * ... + * + * 57234 + * 57235 + * 57236 + * + * ... + * </code></pre> + * It is also possible to send the data to netcat: {@code nc -l 8080} and verify the problem is + * on the client side. + * + * @author Stepan Vavra (stepan.vavra at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class LargeDataTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(LargeDataTest.class.getName()); + private static final int LONG_DATA_SIZE = 1_000_000; // for large set around 5GB, try e.g.: 536_870_912; + private static volatile Throwable exception; + + private static StreamingOutput longData(long sequence) { + return out -> { + long offset = 0; + while (offset < sequence) { + out.write(Long.toString(offset).getBytes()); + out.write('\n'); + offset++; + } + }; + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + @Test + public void postWithLargeData() throws Throwable { + WebTarget webTarget = target("test"); + + Response response = webTarget.request().post(Entity.entity(longData(LONG_DATA_SIZE), MediaType.TEXT_PLAIN_TYPE)); + + try { + if (exception != null) { + + // the reason to throw the exception is that IntelliJ gives you an option to compare the expected with the actual + throw exception; + } + + Assert.assertEquals("Unexpected error: " + response.getStatus(), + Status.Family.SUCCESSFUL, + response.getStatusInfo().getFamily()); + } finally { + response.close(); + } + } + + @Path("/test") + public static class HttpMethodResource { + + @POST + public Response post(InputStream content) { + try { + + longData(LONG_DATA_SIZE).write(new OutputStream() { + + private long position = 0; +// private long mbRead = 0; + + @Override + public void write(final int generated) throws IOException { + int received = content.read(); + + if (received != generated) { + throw new IOException("Bytes don't match at position " + position + + ": received=" + received + + ", generated=" + generated); + } + + position++; +// if (position % (1024 * 1024) == 0) { +// mbRead++; +// System.out.println("MB read: " + mbRead); +// } + } + }); + } catch (IOException e) { + exception = e; + throw new ServerErrorException(e.getMessage(), 500, e); + } + + return Response.ok().build(); + } + + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/ManagedClientTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/ManagedClientTest.java new file mode 100644 index 0000000..2f1f1c3 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/ManagedClientTest.java
@@ -0,0 +1,268 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ClientBinding; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.Uri; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Jersey programmatic managed client test + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ManagedClientTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName()); + + /** + * Managed client configuration for client A. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @ClientBinding(configClass = MyClientAConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public static @interface ClientA { + } + + /** + * Managed client configuration for client B. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @ClientBinding(configClass = MyClientBConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface ClientB { + } + + /** + * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance + * to every method that is annotated with {@link Require @Require} internal feature + * annotation. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + public static class CustomHeaderFeature implements DynamicFeature { + + /** + * A method annotation to be placed on those resource methods to which a validating + * {@link CustomHeaderFilter} instance should be added. + */ + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target(ElementType.METHOD) + public static @interface Require { + + /** + * Expected custom header name to be validated by the {@link CustomHeaderFilter}. + */ + public String headerName(); + + /** + * Expected custom header value to be validated by the {@link CustomHeaderFilter}. + */ + public String headerValue(); + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class); + if (va != null) { + context.register(new CustomHeaderFilter(va.headerName(), va.headerValue())); + } + } + } + + /** + * A filter for appending and validating custom headers. + * <p> + * On the client side, appends a new custom request header with a configured name and value to each outgoing request. + * </p> + * <p> + * On the server side, validates that each request has a custom header with a configured name and value. + * If the validation fails a HTTP 403 response is returned. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter { + + private final String headerName; + private final String headerValue; + + public CustomHeaderFilter(String headerName, String headerValue) { + if (headerName == null || headerValue == null) { + throw new IllegalArgumentException("Header name and value must not be null."); + } + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public void filter(ContainerRequestContext ctx) throws IOException { // validate + if (!headerValue.equals(ctx.getHeaderString(headerName))) { + ctx.abortWith(Response.status(Response.Status.FORBIDDEN) + .type(MediaType.TEXT_PLAIN) + .entity(String + .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue)) + .build()); + } + } + + @Override + public void filter(ClientRequestContext ctx) throws IOException { // append + ctx.getHeaders().putSingle(headerName, headerValue); + } + } + + /** + * Internal resource accessed from the managed client resource. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @Path("internal") + public static class InternalResource { + + @GET + @Path("a") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a") + public String getA() { + return "a"; + } + + @GET + @Path("b") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b") + public String getB() { + return "b"; + } + } + + /** + * A resource that uses managed clients to retrieve values of internal + * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter} + * and require a specific custom header in a request to be set to a specific value. + * <p> + * Properly configured managed clients have a {@code CustomHeaderFilter} instance + * configured to insert the {@link CustomHeaderFeature.Require required} custom header + * with a proper value into the outgoing client requests. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @Path("public") + public static class PublicResource { + + @Uri("a") + @ClientA // resolves to <base>/internal/a + private WebTarget targetA; + + @GET + @Produces("text/plain") + @Path("a") + public String getTargetA() { + return targetA.request(MediaType.TEXT_PLAIN).get(String.class); + } + + @GET + @Produces("text/plain") + @Path("b") + public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) { + return targetB.request(MediaType.TEXT_PLAIN).get(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class) + .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal"); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + public static class MyClientAConfig extends ClientConfig { + + public MyClientAConfig() { + this.register(new CustomHeaderFilter("custom-header", "a")); + } + } + + public static class MyClientBConfig extends ClientConfig { + + public MyClientBConfig() { + this.register(new CustomHeaderFilter("custom-header", "b")); + } + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + /** + * Test that a connection via managed clients works properly. + * + * @throws Exception in case of test failure. + */ + @Test + public void testManagedClient() throws Exception { + final WebTarget resource = target().path("public").path("{name}"); + Response response; + + response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("a", response.readEntity(String.class)); + + response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("b", response.readEntity(String.class)); + } + +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/NoEntityTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/NoEntityTest.java new file mode 100644 index 0000000..a162e14 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/NoEntityTest.java
@@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class NoEntityTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @GET + public Response get() { + return Response.status(Status.CONFLICT).build(); + } + + @POST + public void post(String entity) { + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + @Test + public void testGet() { + WebTarget r = target("test"); + + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testGetWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testPost() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + } + } + + @Test + public void testPostWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + cr.close(); + } + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/RetryHandlerTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/RetryHandlerTest.java new file mode 100644 index 0000000..f779d12 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/RetryHandlerTest.java
@@ -0,0 +1,108 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.apache.http.client.HttpRequestRetryHandler; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class RetryHandlerTest extends JerseyTest { + private static final int READ_TIMEOUT_MS = 100; + + @Override + protected Application configure() { + return new ResourceConfig(RetryHandlerResource.class); + } + + @Path("/") + public static class RetryHandlerResource { + private static volatile int postRequestNumber = 0; + private static volatile int getRequestNumber = 0; + + // Cause a timeout on the first GET and POST request + @GET + public String get(@Context HttpHeaders h) { + if (getRequestNumber++ == 0) { + try { + Thread.sleep(READ_TIMEOUT_MS * 10); + } catch (InterruptedException ex) { + // ignore + } + } + return "GET"; + } + + @POST + public String post(@Context HttpHeaders h, String e) { + if (postRequestNumber++ == 0) { + try { + Thread.sleep(READ_TIMEOUT_MS * 10); + } catch (InterruptedException ex) { + // ignore + } + } + return "POST"; + } + } + + @Test + public void testRetryGet() throws IOException { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + cc.property(ApacheClientProperties.RETRY_HANDLER, + (HttpRequestRetryHandler) (exception, executionCount, context) -> true); + cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + assertEquals("GET", r.request().get(String.class)); + } + + @Test + public void testRetryPost() throws IOException { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new ApacheConnectorProvider()); + cc.property(ApacheClientProperties.RETRY_HANDLER, + (HttpRequestRetryHandler) (exception, executionCount, context) -> true); + cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + assertEquals("POST", r.request() + .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED) + .post(Entity.text("POST"), String.class)); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/SpecialHeaderTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/SpecialHeaderTest.java new file mode 100644 index 0000000..25bfadb --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/SpecialHeaderTest.java
@@ -0,0 +1,89 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.message.GZipEncoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +/** + * + * @author Miroslav Fuksa + */ +public class SpecialHeaderTest extends JerseyTest { + @Override + protected Application configure() { + return new ResourceConfig(MyResource.class, GZipEncoder.class, LoggingFeature.class); + } + + @Path("resource") + public static class MyResource { + @GET + @Produces("text/plain") + @Path("encoded") + public Response getEncoded() { + return Response.ok("get").header(HttpHeaders.CONTENT_ENCODING, "gzip").build(); + } + + @GET + @Produces("text/plain") + @Path("non-encoded") + public Response getNormal() { + return Response.ok("get").build(); + } + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + + @Test + @Ignore("Apache connector does not provide information about encoding for gzip and deflate encoding") + public void testEncoded() { + final Response response = target().path("resource/encoded").request("text/plain").get(); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals("get", response.readEntity(String.class)); + Assert.assertEquals("gzip", response.getHeaderString(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE)); + Assert.assertEquals(3, response.getHeaderString(HttpHeaders.CONTENT_LENGTH)); + } + + @Test + public void testNonEncoded() { + final Response response = target().path("resource/non-encoded").request("text/plain").get(); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals("get", response.readEntity(String.class)); + Assert.assertNull(response.getHeaderString(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE)); + Assert.assertEquals("3", response.getHeaderString(HttpHeaders.CONTENT_LENGTH)); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/StreamingTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/StreamingTest.java new file mode 100644 index 0000000..546c2c3 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/StreamingTest.java
@@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.io.IOException; +import java.io.InputStream; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; + +import javax.inject.Singleton; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ChunkedOutput; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class StreamingTest extends JerseyTest { + + /** + * Test that a data stream can be terminated from the client side. + */ + @Test + public void clientCloseTest() throws IOException { + // start streaming + InputStream inputStream = target().path("/streamingEndpoint").request().get(InputStream.class); + + WebTarget sendTarget = target().path("/streamingEndpoint/send"); + // trigger sending 'A' to the stream; OK is sent if everything on the server was OK + assertEquals("OK", sendTarget.request().get().readEntity(String.class)); + // check 'A' has been sent + assertEquals('A', inputStream.read()); + // closing the stream should tear down the connection + inputStream.close(); + // trigger sending another 'A' to the stream; it should fail + // (indicating that the streaming has been terminated on the server) + assertEquals("NOK", sendTarget.request().get().readEntity(String.class)); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new ApacheConnectorProvider()); + } + + @Override + protected Application configure() { + return new ResourceConfig(StreamingEndpoint.class); + } + + @Singleton + @Path("streamingEndpoint") + public static class StreamingEndpoint { + + private final ChunkedOutput<String> output = new ChunkedOutput<>(String.class); + + @GET + @Path("send") + public String sendEvent() { + try { + output.write("A"); + } catch (IOException e) { + return "NOK"; + } + + return "OK"; + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + public ChunkedOutput<String> get() { + return output; + } + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/TimeoutTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/TimeoutTest.java new file mode 100644 index 0000000..66860cf --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/TimeoutTest.java
@@ -0,0 +1,104 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.net.SocketTimeoutException; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Martin Matula + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class TimeoutTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName()); + + @Path("/test") + public static class TimeoutResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("timeout") + public String getTimeout() { + try { + Thread.sleep(2000); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + return "GET"; + } + } + + @Override + protected Application configure() { + final ResourceConfig config = new ResourceConfig(TimeoutResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(final ClientConfig config) { + config.property(ClientProperties.READ_TIMEOUT, 1000); + config.connectorProvider(new ApacheConnectorProvider()); + } + + @Test + public void testFast() { + final Response r = target("test").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testSlow() { + try { + target("test/timeout").request().get(); + fail("Timeout expected."); + } catch (final ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(SocketTimeoutException.class)); + } + } + + @Test + public void testPerRequestTimeout() { + final Response r = target("test/timeout").request() + .property(ClientProperties.READ_TIMEOUT, 3000).get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/TraceSupportTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/TraceSupportTest.java new file mode 100644 index 0000000..4691bb9 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/TraceSupportTest.java
@@ -0,0 +1,235 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This very basic resource showcases support of a HTTP TRACE method, + * not directly supported by JAX-RS API. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class TraceSupportTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName()); + + /** + * Programmatic tracing root resource path. + */ + public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic"; + + /** + * Annotated class-based tracing root resource path. + */ + public static final String ROOT_PATH_ANNOTATED = "tracing/annotated"; + + @HttpMethod(TRACE.NAME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TRACE { + public static final String NAME = "TRACE"; + } + + @Path(ROOT_PATH_ANNOTATED) + public static class TracingResource { + + @TRACE + @Produces("text/plain") + public String trace(Request request) { + return stringify((ContainerRequest) request); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TracingResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC); + resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector<ContainerRequestContext, Response>() { + + @Override + public Response apply(ContainerRequestContext request) { + if (request == null) { + return Response.noContent().build(); + } else { + return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build(); + } + } + }); + + return config.registerResources(resourceBuilder.build()); + + } + + private String[] expectedFragmentsProgrammatic = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic" + }; + private String[] expectedFragmentsAnnotated = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/annotated" + }; + + private WebTarget prepareTarget(String path) { + final WebTarget target = target(); + target.register(LoggingFeature.class); + return target.path(path); + } + + @Test + public void testProgrammaticApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsProgrammatic) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testAnnotatedApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsAnnotated) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testTraceWithEntity() throws Exception { + _testTraceWithEntity(false, false); + } + + @Test + public void testAsyncTraceWithEntity() throws Exception { + _testTraceWithEntity(true, false); + } + + @Test + public void testTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(false, true); + } + + @Test + public void testAsyncTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(true, true); + } + + private void _testTraceWithEntity(final boolean isAsync, final boolean useApacheConnection) throws Exception { + try { + WebTarget target = useApacheConnection ? getApacheClient().target(target().getUri()) : target(); + target = target.path(ROOT_PATH_ANNOTATED); + + final Entity<String> entity = Entity.entity("trace", MediaType.WILDCARD_TYPE); + + Response response; + if (!isAsync) { + response = target.request().method(TRACE.NAME, entity); + } else { + response = target.request().async().method(TRACE.NAME, entity).get(); + } + + fail("A TRACE request MUST NOT include an entity. (response=" + response + ")"); + } catch (Exception e) { + // OK + } + } + + private Client getApacheClient() { + return ClientBuilder.newClient(new ClientConfig().connectorProvider(new ApacheConnectorProvider())); + } + + + public static String stringify(ContainerRequest request) { + StringBuilder buffer = new StringBuilder(); + + printRequestLine(buffer, request); + printPrefixedHeaders(buffer, request.getHeaders()); + + if (request.hasEntity()) { + buffer.append(request.readEntity(String.class)).append("\n"); + } + + return buffer.toString(); + } + + private static void printRequestLine(StringBuilder buffer, ContainerRequest request) { + buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n"); + } + + private static void printPrefixedHeaders(StringBuilder buffer, Map<String, List<String>> headers) { + for (Map.Entry<String, List<String>> e : headers.entrySet()) { + List<String> val = e.getValue(); + String header = e.getKey(); + + if (val.size() == 1) { + buffer.append(header).append(": ").append(val.get(0)).append("\n"); + } else { + StringBuilder sb = new StringBuilder(); + boolean add = false; + for (String s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + buffer.append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/UnderlyingCookieStoreAccessTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/UnderlyingCookieStoreAccessTest.java new file mode 100644 index 0000000..3238c81 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/UnderlyingCookieStoreAccessTest.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.jersey.client.ClientConfig; + +import org.apache.http.client.CookieStore; +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * Test of access to the underlying CookieStore instance used by the connector. + * + * @author Maksim Mukosey (mmukosey at gmail.com) + */ +public class UnderlyingCookieStoreAccessTest { + + @Test + public void testCookieStoreInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new ApacheConnectorProvider())); + final CookieStore csOnClient = ApacheConnectorProvider.getCookieStore(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getCookieStore method above. + final WebTarget target = client.target("http://localhost/"); + final CookieStore csOnTarget = ApacheConnectorProvider.getCookieStore(target); + + assertNotNull("CookieStore instance set on JerseyClient should not be null.", csOnClient); + assertNotNull("CookieStore instance set on JerseyWebTarget should not be null.", csOnTarget); + assertSame("CookieStore instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget" + + "(provided the target instance has not been further configured).", csOnClient, csOnTarget); + } +}
diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/UnderlyingHttpClientAccessTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/UnderlyingHttpClientAccessTest.java new file mode 100644 index 0000000..a5a0299 --- /dev/null +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.jersey.client.ClientConfig; + +import org.apache.http.client.HttpClient; +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * Test of access to the underlying HTTP client instance used by the connector. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class UnderlyingHttpClientAccessTest { + + /** + * Verifier of JERSEY-2424 fix. + */ + @Test + public void testHttpClientInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new ApacheConnectorProvider())); + final HttpClient hcOnClient = ApacheConnectorProvider.getHttpClient(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getHttpClient method above. + final WebTarget target = client.target("http://localhost/"); + final HttpClient hcOnTarget = ApacheConnectorProvider.getHttpClient(target); + + assertNotNull("HTTP client instance set on JerseyClient should not be null.", hcOnClient); + assertNotNull("HTTP client instance set on JerseyWebTarget should not be null.", hcOnTarget); + assertSame("HTTP client instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget" + + "(provided the target instance has not been further configured).", + hcOnClient, hcOnTarget + ); + } +}
diff --git a/connectors/grizzly-connector/pom.xml b/connectors/grizzly-connector/pom.xml new file mode 100644 index 0000000..eb4dea9 --- /dev/null +++ b/connectors/grizzly-connector/pom.xml
@@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-grizzly-connector</artifactId> + <packaging>jar</packaging> + <name>jersey-connectors-grizzly</name> + + <description>Jersey Client Transport via Grizzly</description> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.glassfish.grizzly</groupId> + <artifactId>grizzly-http-client</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.grizzly</groupId> + <artifactId>grizzly-websockets</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.grizzly</groupId> + <artifactId>connection-pool</artifactId> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-bundle</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project>
diff --git a/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/GrizzlyConnector.java b/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/GrizzlyConnector.java new file mode 100644 index 0000000..6b3a3bc --- /dev/null +++ b/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/GrizzlyConnector.java
@@ -0,0 +1,479 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.internal.Version; +import org.glassfish.jersey.internal.util.collection.ByteBufferInputStream; +import org.glassfish.jersey.internal.util.collection.NonBlockingInputStream; +import org.glassfish.jersey.message.internal.HeaderUtils; +import org.glassfish.jersey.message.internal.OutboundMessageContext; + +import org.glassfish.grizzly.memory.Buffers; +import org.glassfish.grizzly.memory.MemoryManager; + +import com.ning.http.client.AsyncHandler; +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.HttpResponseBodyPart; +import com.ning.http.client.HttpResponseHeaders; +import com.ning.http.client.HttpResponseStatus; +import com.ning.http.client.ProxyServerSelector; +import com.ning.http.client.Request; +import com.ning.http.client.RequestBuilder; +import com.ning.http.client.providers.grizzly.FeedableBodyGenerator; +import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider; +import com.ning.http.util.ProxyUtils; + +/** + * The transport using the AsyncHttpClient. + * + * @author Stepan Kopriva + * @author Marek Potociar (marek.potociar at oracle.com) + */ +class GrizzlyConnector implements Connector { + + private final AsyncHttpClient grizzlyClient; + + /** + * Create new connector based on Grizzly asynchronous client library. + * + * @param client Jersey client instance to create the connector for. + * @param config Jersey client runtime configuration to be used to configure the connector parameters. + * @param asyncClientCustomizer Async HTTP Client configuration builder customizer. + */ + GrizzlyConnector(final Client client, + final Configuration config, + final GrizzlyConnectorProvider.AsyncClientCustomizer asyncClientCustomizer) { + AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder(); + + ExecutorService executorService; + if (config != null) { + final Object threadPoolSize = config.getProperties().get(ClientProperties.ASYNC_THREADPOOL_SIZE); + + if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { + executorService = Executors.newFixedThreadPool((Integer) threadPoolSize); + } else { + executorService = Executors.newCachedThreadPool(); + } + + builder = builder.setExecutorService(executorService); + + builder.setConnectTimeout(ClientProperties.getValue(config.getProperties(), + ClientProperties.CONNECT_TIMEOUT, 10000)); + + builder.setRequestTimeout(ClientProperties.getValue(config.getProperties(), + ClientProperties.READ_TIMEOUT, 10000)); + + Object proxyUri; + proxyUri = config.getProperty(ClientProperties.PROXY_URI); + if (proxyUri != null) { + final URI u = getProxyUri(proxyUri); + final Properties proxyProperties = new Properties(); + proxyProperties.setProperty(ProxyUtils.PROXY_PROTOCOL, u.getScheme()); + proxyProperties.setProperty(ProxyUtils.PROXY_HOST, u.getHost()); + proxyProperties.setProperty(ProxyUtils.PROXY_PORT, String.valueOf(u.getPort())); + + final String userName = ClientProperties.getValue( + config.getProperties(), ClientProperties.PROXY_USERNAME, String.class); + if (userName != null) { + proxyProperties.setProperty(ProxyUtils.PROXY_USER, userName); + + final String password = ClientProperties.getValue( + config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class); + if (password != null) { + proxyProperties.setProperty(ProxyUtils.PROXY_PASSWORD, password); + } + } + ProxyServerSelector proxyServerSelector = ProxyUtils.createProxyServerSelector(proxyProperties); + builder.setProxyServerSelector(proxyServerSelector); + } + } else { + executorService = Executors.newCachedThreadPool(); + builder.setExecutorService(executorService); + } + + builder.setAllowPoolingConnections(true); + if (client.getSslContext() != null) { + builder.setSSLContext(client.getSslContext()); + } + if (client.getHostnameVerifier() != null) { + builder.setHostnameVerifier(client.getHostnameVerifier()); + } + + if (asyncClientCustomizer != null) { + builder = asyncClientCustomizer.customize(client, config, builder); + } + + AsyncHttpClientConfig asyncClientConfig = builder.build(); + + this.grizzlyClient = new AsyncHttpClient(new GrizzlyAsyncHttpProvider(asyncClientConfig), asyncClientConfig); + } + + @SuppressWarnings("ChainOfInstanceofChecks") + private static URI getProxyUri(final Object proxy) { + if (proxy instanceof URI) { + return (URI) proxy; + } else if (proxy instanceof String) { + return URI.create((String) proxy); + } else { + throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI)); + } + } + + /** + * Get the underlying Grizzly {@link com.ning.http.client.AsyncHttpClient} instance. + * + * @return underlying Grizzly {@link com.ning.http.client.AsyncHttpClient} instance. + */ + public AsyncHttpClient getGrizzlyClient() { + return grizzlyClient; + } + + /** + * Sends the {@link javax.ws.rs.core.Request} via Grizzly transport and returns the {@link javax.ws.rs.core.Response}. + * + * @param request Jersey client request to be sent. + * @return received response. + */ + @Override + public ClientResponse apply(final ClientRequest request) { + final Request connectorRequest = translate(request); + final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(request.getHeaders(), connectorRequest); + + final CompletableFuture<ClientResponse> responseFuture = new CompletableFuture<>(); + final ByteBufferInputStream entityStream = new ByteBufferInputStream(); + final AtomicBoolean futureSet = new AtomicBoolean(false); + + try { + grizzlyClient.executeRequest(connectorRequest, new AsyncHandler<Void>() { + private volatile HttpResponseStatus status = null; + + @Override + public STATE onStatusReceived(final HttpResponseStatus responseStatus) throws Exception { + status = responseStatus; + return STATE.CONTINUE; + } + + @Override + public STATE onHeadersReceived(HttpResponseHeaders headers) throws Exception { + if (!futureSet.compareAndSet(false, true)) { + return STATE.ABORT; + } + + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, request.getHeaders(), + GrizzlyConnector.this.getClass().getName()); + + responseFuture.complete(translate(request, this.status, headers, entityStream)); + return STATE.CONTINUE; + } + + @Override + public STATE onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception { + entityStream.put(bodyPart.getBodyByteBuffer()); + return STATE.CONTINUE; + } + + @Override + public Void onCompleted() throws Exception { + entityStream.closeQueue(); + return null; + } + + @Override + public void onThrowable(Throwable t) { + entityStream.closeQueue(t); + + if (futureSet.compareAndSet(false, true)) { + t = t instanceof IOException ? new ProcessingException(t.getMessage(), t) : t; + responseFuture.completeExceptionally(t); + } + } + }); + + return responseFuture.get(); + } catch (ExecutionException ex) { + Throwable e = ex.getCause() == null ? ex : ex.getCause(); + throw new ProcessingException(e.getMessage(), e); + } catch (InterruptedException ex) { + throw new ProcessingException(ex.getMessage(), ex); + } + } + + @Override + public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) { + final Request connectorRequest = translate(request); + final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(request.getHeaders(), connectorRequest); + final ByteBufferInputStream entityStream = new ByteBufferInputStream(); + final AtomicBoolean callbackInvoked = new AtomicBoolean(false); + + Throwable failure; + try { + return grizzlyClient.executeRequest(connectorRequest, new AsyncHandler<Void>() { + private volatile HttpResponseStatus status = null; + + @Override + public STATE onStatusReceived(final HttpResponseStatus responseStatus) throws Exception { + status = responseStatus; + return STATE.CONTINUE; + } + + @Override + public STATE onHeadersReceived(HttpResponseHeaders headers) throws Exception { + if (!callbackInvoked.compareAndSet(false, true)) { + return STATE.ABORT; + } + + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, request.getHeaders(), + GrizzlyConnector.this.getClass().getName()); + // hand-off to grizzly's application thread pool for response processing + processResponse(new Runnable() { + @Override + public void run() { + callback.response(translate(request, status, headers, entityStream)); + } + }); + return STATE.CONTINUE; + } + + @Override + public STATE onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception { + entityStream.put(bodyPart.getBodyByteBuffer()); + return STATE.CONTINUE; + } + + @Override + public Void onCompleted() throws Exception { + entityStream.closeQueue(); + return null; + } + + @Override + public void onThrowable(Throwable t) { + entityStream.closeQueue(t); + + if (callbackInvoked.compareAndSet(false, true)) { + t = t instanceof IOException ? new ProcessingException(t.getMessage(), t) : t; + callback.failure(t); + } + } + }); + } catch (Throwable t) { + failure = t; + } + + if (callbackInvoked.compareAndSet(false, true)) { + callback.failure(failure); + } + CompletableFuture<Object> future = new CompletableFuture<>(); + future.completeExceptionally(failure); + return future; + } + + @Override + public void close() { + grizzlyClient.close(); + } + + private ClientResponse translate(final ClientRequest requestContext, + final HttpResponseStatus status, + final HttpResponseHeaders headers, + final NonBlockingInputStream entityStream) { + + final ClientResponse responseContext = new ClientResponse(new Response.StatusType() { + @Override + public int getStatusCode() { + return status.getStatusCode(); + } + + @Override + public Response.Status.Family getFamily() { + return Response.Status.Family.familyOf(status.getStatusCode()); + } + + @Override + public String getReasonPhrase() { + return status.getStatusText(); + } + }, requestContext); + + for (Map.Entry<String, List<String>> entry : headers.getHeaders().entrySet()) { + for (String value : entry.getValue()) { + responseContext.getHeaders().add(entry.getKey(), value); + } + } + + responseContext.setEntityStream(entityStream); + + return responseContext; + } + + private com.ning.http.client.Request translate(final ClientRequest requestContext) { + final String strMethod = requestContext.getMethod(); + final URI uri = requestContext.getUri(); + + RequestBuilder builder = new RequestBuilder(strMethod).setUrl(uri.toString()); + + builder.setFollowRedirects(requestContext.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true)); + + if (requestContext.hasEntity()) { + + final RequestEntityProcessing entityProcessing = + requestContext.resolveProperty(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + + if (entityProcessing == RequestEntityProcessing.BUFFERED) { + byte[] entityBytes = bufferEntity(requestContext); + builder = builder.setBody(entityBytes); + } else { + final FeedableBodyGenerator bodyGenerator = new FeedableBodyGenerator(); + final Integer chunkSize = requestContext.resolveProperty( + ClientProperties.CHUNKED_ENCODING_SIZE, ClientProperties.DEFAULT_CHUNK_SIZE); + bodyGenerator.setMaxPendingBytes(chunkSize); + final FeedableBodyGenerator.Feeder feeder = new FeedableBodyGenerator.SimpleFeeder(bodyGenerator) { + @Override + public void flush() throws IOException { + requestContext.writeEntity(); + } + }; + requestContext.setStreamProvider(new OutboundMessageContext.StreamProvider() { + + @Override + public OutputStream getOutputStream(int contentLength) throws IOException { + return new FeederAdapter(feeder); + } + }); + bodyGenerator.setFeeder(feeder); + builder.setBody(bodyGenerator); + } + } + + final GrizzlyConnectorProvider.RequestCustomizer requestCustomizer = requestContext.resolveProperty( + GrizzlyConnectorProvider.REQUEST_CUSTOMIZER, + GrizzlyConnectorProvider.RequestCustomizer.class); + if (requestCustomizer != null) { + builder = requestCustomizer.customize(requestContext, builder); + } + + return builder.build(); + } + + /** + * Submits the response processing on Grizzly client's application thread pool. + * + * @param responseTask task to be processed on application thread pool. + */ + private void processResponse(Runnable responseTask) { + this.grizzlyClient.getConfig().executorService().submit(responseTask); + } + + /** + * Utility OutputStream implementation that can feed Grizzly chunk-encoded body generator. + */ + private class FeederAdapter extends OutputStream { + + final FeedableBodyGenerator.Feeder delegate; + + /** + * Get me a new adapter for given feeder. + * + * @param bodyFeeder adaptee to get fed as an output stream. + */ + FeederAdapter(FeedableBodyGenerator.Feeder bodyFeeder) { + this.delegate = bodyFeeder; + } + + @Override + public void write(int b) throws IOException { + final byte[] buffer = new byte[1]; + buffer[0] = (byte) b; + delegate.feed(Buffers.wrap(MemoryManager.DEFAULT_MEMORY_MANAGER, buffer), false); + } + + @Override + public void write(byte[] b) throws IOException { + delegate.feed(Buffers.wrap(MemoryManager.DEFAULT_MEMORY_MANAGER, b), false); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.feed(Buffers.wrap(MemoryManager.DEFAULT_MEMORY_MANAGER, b, off, len), false); + } + + @Override + public void close() throws IOException { + delegate.feed(Buffers.EMPTY_BUFFER, true); + } + } + + @SuppressWarnings("MagicNumber") + private byte[] bufferEntity(ClientRequest requestContext) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + requestContext.setStreamProvider(new OutboundMessageContext.StreamProvider() { + + @Override + public OutputStream getOutputStream(int contentLength) throws IOException { + return baos; + } + }); + try { + requestContext.writeEntity(); + } catch (IOException e) { + throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e); + } + return baos.toByteArray(); + } + + private static Map<String, String> writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, + final com.ning.http.client.Request request) { + Map<String, String> stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers); + + for (Map.Entry<String, String> e : stringHeaders.entrySet()) { + request.getHeaders().add(e.getKey(), e.getValue()); + } + return stringHeaders; + } + + @Override + public String getName() { + return String.format("Async HTTP Grizzly Connector %s", Version.getVersion()); + } +}
diff --git a/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/GrizzlyConnectorProvider.java b/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/GrizzlyConnectorProvider.java new file mode 100644 index 0000000..ab39857 --- /dev/null +++ b/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/GrizzlyConnectorProvider.java
@@ -0,0 +1,273 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Configurable; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.Initializable; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.internal.util.Property; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.RequestBuilder; + +/** + * Connector provider for Jersey {@link Connector connectors} that utilize + * Grizzly Asynchronous HTTP Client to send and receive HTTP request and responses. + * <p> + * The following connector configuration properties are supported: + * <ul> + * <li>{@link org.glassfish.jersey.client.ClientProperties#CONNECT_TIMEOUT}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} + * - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li> + * </ul> + * </p> + * <p> + * Connector instances created via this connector provider use + * {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED chunked encoding} as a default setting. + * This can be overridden by the {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING}. + * </p> + * <p> + * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an entity is not read from the response then + * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called after processing the response to release + * connection-based resources. + * </p> + * <p> + * If a response entity is obtained that is an instance of {@link java.io.Closeable} then the instance MUST + * be closed after processing the entity to release connection-based resources. + * <p/> + * <p> + * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE. + * <p/> + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.5 + */ +public class GrizzlyConnectorProvider implements ConnectorProvider { + /** + * A {@link GrizzlyConnectorProvider.RequestCustomizer request customizer} instance to be used to customize the + * request. + * + * The value MUST be an instance implementing the {@link GrizzlyConnectorProvider.RequestCustomizer} SPI. + * <p> + * A default value is not set (is {@code null}). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @see #register(Invocation.Builder, GrizzlyConnectorProvider.RequestCustomizer) + * @see org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider.RequestCustomizer + */ + @Property + static final String REQUEST_CUSTOMIZER = "jersey.config.grizzly.client.request.customizer"; + + private final AsyncClientCustomizer asyncClientCustomizer; + + /** + * A customization SPI for the async client instance underlying Grizzly connectors. + * <p> + * An implementation of async client customizer can be + * registered in a {@code GrizzlyConnectorProvider} + * {@link GrizzlyConnectorProvider#GrizzlyConnectorProvider(GrizzlyConnectorProvider.AsyncClientCustomizer) constructor}. + * When a connector instance is then created, the customizer is invoked to update the + * {@link com.ning.http.client.AsyncHttpClientConfig.Builder underlying async client configuration builder} before the actual + * configuration instance is built and used to create the async client instance. + * The customizer thus provides a way how to configure parts of the underlying async client SPI that are not directly + * exposed in the {@code GrizzlyConnectorProvider} API. + * </p> + * + * @see org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider.RequestCustomizer + * @since 2.10 + */ + public static interface AsyncClientCustomizer { + /** + * Customize the underlying asynchronous client configuration builder. + * <p> + * The configuration builder instance instance returned from the method will be subsequently used to build the + * configuration object that configures both the {@link com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider} + * Grizzly async client provider} as well as the underlying {@link com.ning.http.client.AsyncHttpClient async HTTP + * client} instance itself. + * </p> + * <p> + * Note that any JAX-RS and Jersey specific configuration updates on the configuration builder happen before this method + * is invoked. As such, changes made to the configuration builder may override or cancel the effect of the JAX-RS and + * Jersey specific configuration changes. As such any configuration changes should be made with care and implementers + * should be aware of possible side effect of their changes. + * </p> + * + * @param client JAX-RS client for which the connector is being created. + * @param config JAX-RS configuration that was used to initialize connector's configuration. + * @param configBuilder Async HTTP Client configuration builder that has been initialized based on the JAX-RS + * configuration. + * @return Async HTTP Client builder instance to be used to configure the underlying Grizzly provider and async HTTP + * client instance. Typically, the method returns the same {@code configBuilder} instance that has been passed into + * the method as an input parameter, but it is not required to do so. + */ + public AsyncHttpClientConfig.Builder customize(final Client client, + final Configuration config, + final AsyncHttpClientConfig.Builder configBuilder); + } + + /** + * A customization SPI for the async client request instances. + * + * A request customizer can be used to configure Async HTTP Client specific details of the request, which are not directly + * exposed via the JAX-RS, Jersey or Grizzly connector provider API. + * <p> + * Before a request is built and sent for execution, a registered request customizer + * implementation can update the Async HTTP Client {@link com.ning.http.client.RequestBuilder request builder} used + * to build the request instance ultimately sent for processing. + * An instance of the request customizer can be either {@link #register(ClientConfig, RequestCustomizer) registered globally} + * for all requests by registering the customizer in the Jersey client configuration, or it can be individually + * {@link #register(Invocation.Builder, RequestCustomizer) registered per request}, by registering it into a specific + * invocation builder instance. In case of a conflict when one instance is registered globally and another per request, the + * per request registered customizer takes precedence and the global customizer will be ignored. + * </p> + * + * @see org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider.AsyncClientCustomizer + * @see #register(org.glassfish.jersey.client.ClientConfig, GrizzlyConnectorProvider.RequestCustomizer) + * @see #register(Invocation.Builder, GrizzlyConnectorProvider.RequestCustomizer) + * @since 2.10 + */ + public static interface RequestCustomizer { + /** + * Customize the underlying Async HTTP Client request builder associated with a specific Jersey client request. + * <p> + * The request builder instance returned from the method will be subsequently used to build the actual Async HTTP Client + * request instance sent for execution. + * </p> + * <p> + * Note that any JAX-RS and Jersey specific request configuration updates on the request builder happen before this + * method is invoked. As such, changes made to the request builder may override or cancel the effect of the JAX-RS and + * Jersey specific request configuration changes. As such any request builder changes should be made with care and + * implementers should be aware of possible side effect of their changes. + * </p> + * + * @param requestContext Jersey client request instance for which the Async HTTP Client request is being built. + * @param requestBuilder Async HTTP Client request builder for the Jersey request. + * @return Async HTTP Client request builder instance that will be used to build the actual Async HTTP Client + * request instance sent for execution. Typically, the method returns the same {@code requestBuilder} instance that + * has been passed into the method as an input parameter, but it is not required to do so. + */ + public RequestBuilder customize(final ClientRequest requestContext, final RequestBuilder requestBuilder); + } + + /** + * Create new Grizzly Async HTTP Client connector provider. + */ + public GrizzlyConnectorProvider() { + this.asyncClientCustomizer = null; + } + + /** + * Create new Grizzly Async HTTP Client connector provider with a custom client configuration customizer. + * + * @param asyncClientCustomizer Async HTTP Client configuration customizer. + * @since 2.10 + */ + public GrizzlyConnectorProvider(final AsyncClientCustomizer asyncClientCustomizer) { + this.asyncClientCustomizer = asyncClientCustomizer; + } + + @Override + public Connector getConnector(Client client, Configuration config) { + return new GrizzlyConnector(client, config, asyncClientCustomizer); + } + + /** + * Retrieve the underlying Grizzly {@link AsyncHttpClient} instance from + * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget} + * configured to use {@code GrizzlyConnectorProvider}. + * + * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use + * {@code GrizzlyConnectorProvider}. + * @return underlying Grizzly {@code AsyncHttpClient} instance. + * + * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient} + * nor {@code JerseyWebTarget} instance or in case the component + * is not configured to use a {@code GrizzlyConnectorProvider}. + * @since 2.8 + */ + public static AsyncHttpClient getHttpClient(Configurable<?> component) { + if (!(component instanceof Initializable)) { + throw new IllegalArgumentException( + LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName())); + } + + final Initializable<?> initializable = (Initializable<?>) component; + Connector connector = initializable.getConfiguration().getConnector(); + if (connector == null) { + initializable.preInitialize(); + connector = initializable.getConfiguration().getConnector(); + } + + if (connector instanceof GrizzlyConnector) { + return ((GrizzlyConnector) connector).getGrizzlyClient(); + } + + throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED()); + } + + /** + * Register a request customizer for a single request. + * + * A registered customizer will be used to customize the underlying Async HTTP Client request builder. + * <p> + * Invoking this method on an instance that is not configured to use Grizzly Async HTTP Client + * connector does not have any effect. + * </p> + * + * @param builder JAX-RS request invocation builder. + * @param customizer request customizer to be registered. + * @return updated Jersey client config with the Grizzly + * {@link org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider.RequestCustomizer} attached. + */ + public static Invocation.Builder register(Invocation.Builder builder, RequestCustomizer customizer) { + return builder.property(REQUEST_CUSTOMIZER, customizer); + } + + /** + * Register a request customizer for a all requests executed by a client instance configured with this client config. + * + * A registered customizer will be used to customize underlying Async HTTP Client request builders for all requests created + * using the Jersey client instance configured with this client config. + * <p> + * Invoking this method on an instance that is not configured to use Grizzly Async HTTP Client + * connector does not have any effect. + * </p> + * + * @param config Jersey client configuration. + * @param customizer Async HTTP Client configuration customizer. + * @return updated JAX-RS client invocation builder with the Grizzly + * {@link org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider.RequestCustomizer RequestCustomizer} attached. + */ + public static ClientConfig register(ClientConfig config, RequestCustomizer customizer) { + return config.property(REQUEST_CUSTOMIZER, customizer); + } +}
diff --git a/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/package-info.java b/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/package-info.java new file mode 100644 index 0000000..1e89b00 --- /dev/null +++ b/connectors/grizzly-connector/src/main/java/org/glassfish/jersey/grizzly/connector/package-info.java
@@ -0,0 +1,21 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on the + * Grizzly Async Client. + */ +package org.glassfish.jersey.grizzly.connector;
diff --git a/connectors/grizzly-connector/src/main/resources/org/glassfish/jersey/grizzly/connector/localization.properties b/connectors/grizzly-connector/src/main/resources/org/glassfish/jersey/grizzly/connector/localization.properties new file mode 100644 index 0000000..7fb050e --- /dev/null +++ b/connectors/grizzly-connector/src/main/resources/org/glassfish/jersey/grizzly/connector/localization.properties
@@ -0,0 +1,20 @@ +# +# Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +error.buffering.entity=Error buffering the entity. +expected.connector.provider.not.used=The supplied component is not configured to use a GrizzlyConnectorProvider. +invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget. +wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI.
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/AsyncTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/AsyncTest.java new file mode 100644 index 0000000..78298fc --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/AsyncTest.java
@@ -0,0 +1,200 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.container.TimeoutHandler; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.hamcrest.Matchers; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Asynchronous connector test. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class AsyncTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName()); + private static final String PATH = "async"; + + /** + * Asynchronous test resource. + */ + @Path(PATH) + public static class AsyncResource { + + /** + * Typical long-running operation duration. + */ + public static final long OPERATION_DURATION = 1000; + + /** + * Long-running asynchronous post. + * + * @param asyncResponse async response. + * @param id post request id (received as request payload). + */ + @POST + public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) { + LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName()); + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep() + try { + Thread.sleep(OPERATION_DURATION); + return "DONE-" + id; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED-" + id; + } finally { + LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName()); + } + } + }, "async-post-runner-" + id).start(); + } + + /** + * Long-running async get request that times out. + * + * @param asyncResponse async response. + */ + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName()); + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(final AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + } + }); + asyncResponse.setTimeout(1, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // very expensive operation that typically finishes within 1 second but can take up to 5 seconds, + // simulated using sleep() + try { + Thread.sleep(5 * OPERATION_DURATION); + return "DONE"; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED"; + } finally { + LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName()); + } + } + }).start(); + } + + } + + @Override + protected Application configure() { + return new ResourceConfig(AsyncResource.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + } + + @Override + protected void configureClient(final ClientConfig config) { + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + /** + * Test asynchronous POST. + * <p/> + * Send 3 async POST requests and wait to receive the responses. Check the response content and + * assert that the operation did not take more than twice as long as a single long operation duration + * (this ensures async request execution). + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncPost() throws Exception { + final long tic = System.currentTimeMillis(); + + // Submit requests asynchronously. + final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1")); + final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2")); + final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3")); + // get() waits for the response + final String r1 = rf1.get().readEntity(String.class); + final String r2 = rf2.get().readEntity(String.class); + final String r3 = rf3.get().readEntity(String.class); + + final long toc = System.currentTimeMillis(); + + assertEquals("DONE-1", r1); + assertEquals("DONE-2", r2); + assertEquals("DONE-3", r3); + + assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION)); + } + + /** + * Test accessing an operation that times out on the server. + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncGetWithTimeout() throws Exception { + final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/CustomizersTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/CustomizersTest.java new file mode 100644 index 0000000..b28aae5 --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/CustomizersTest.java
@@ -0,0 +1,132 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.client.Entity.text; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.RequestBuilder; +import com.ning.http.client.filter.FilterContext; +import com.ning.http.client.filter.FilterException; +import com.ning.http.client.filter.RequestFilter; + +/** + * Async HTTP Client Config and Request customizers unit tests. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class CustomizersTest extends JerseyTest { + + @Path("/test") + public static class EchoResource { + @POST + public Response post(@HeaderParam("X-Test-Config") String testConfigHeader, + @HeaderParam("X-Test-Request") String testRequestHeader, + String entity) { + return Response.ok("POSTed " + entity) + .header("X-Test-Config", testConfigHeader) + .header("X-Test-Request", testRequestHeader) + .build(); + } + } + + @Override + protected Application configure() { + return new ResourceConfig(EchoResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + final GrizzlyConnectorProvider connectorProvider = new GrizzlyConnectorProvider( + new GrizzlyConnectorProvider.AsyncClientCustomizer() { + @Override + public AsyncHttpClientConfig.Builder customize(Client client, + Configuration config, + AsyncHttpClientConfig.Builder configBuilder) { + return configBuilder.addRequestFilter(new RequestFilter() { + @Override + public FilterContext filter(FilterContext filterContext) throws FilterException { + filterContext.getRequest().getHeaders().add("X-Test-Config", "tested"); + return filterContext; + } + }); + } + }); + config.connectorProvider(connectorProvider); + GrizzlyConnectorProvider.register(config, new GrizzlyConnectorProvider.RequestCustomizer() { + @Override + public RequestBuilder customize(ClientRequest requestContext, RequestBuilder requestBuilder) { + requestBuilder.addHeader("X-Test-Request", "tested-global"); + return requestBuilder; + } + }); + } + + /** + * Jersey-2540 related test. + */ + @Test + public void testCustomizers() { + Response response; + + // now using global request customizer + response = target("test").request().post(text("echo")); + assertEquals("POSTed echo", response.readEntity(String.class)); + assertEquals("tested", response.getHeaderString("X-Test-Config")); + assertEquals("tested-global", response.getHeaderString("X-Test-Request")); + + + // now using request-specific request customizer + final Invocation.Builder builder = target("test").request(); + GrizzlyConnectorProvider.register(builder, new GrizzlyConnectorProvider.RequestCustomizer() { + @Override + public RequestBuilder customize(ClientRequest requestContext, RequestBuilder requestBuilder) { + requestBuilder.addHeader("X-Test-Request", "tested-per-request"); + return requestBuilder; + } + }); + response = builder.post(text("echo")); + assertEquals("POSTed echo", response.readEntity(String.class)); + assertEquals("tested", response.getHeaderString("X-Test-Config")); + assertEquals("tested-per-request", response.getHeaderString("X-Test-Request")); + + + // now using global request customizer again + response = target("test").request().post(text("echo")); + assertEquals("POSTed echo", response.readEntity(String.class)); + assertEquals("tested", response.getHeaderString("X-Test-Config")); + assertEquals("tested-global", response.getHeaderString("X-Test-Request")); + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/FollowRedirectsTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/FollowRedirectsTest.java new file mode 100644 index 0000000..7c9c417 --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/FollowRedirectsTest.java
@@ -0,0 +1,102 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Grizzly connector follow redirect tests. + * + * @author Martin Matula + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class FollowRedirectsTest extends JerseyTest { + @Path("/test") + public static class RedirectResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("redirect") + public Response redirect() { + return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build(); + } + } + + @Override + protected Application configure() { + return new ResourceConfig(RedirectResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + private static class RedirectTestFilter implements ClientResponseFilter { + public static final String RESOLVED_URI_HEADER = "resolved-uri"; + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (responseContext instanceof ClientResponse) { + ClientResponse clientResponse = (ClientResponse) responseContext; + responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString()); + } + } + } + + @Test + public void testDoFollow() { + Response r = target("test/redirect") + .register(RedirectTestFilter.class) + .request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); +// TODO uncomment as part of JERSEY-2388 fix. +// assertEquals( +// UriBuilder.fromUri(getBaseUri()).path(RedirectResource.class).build().toString(), +// r.getHeaderString(RedirectTestFilter.RESOLVED_URI_HEADER)); + } + + @Test + public void testDontFollow() { + WebTarget t = target("test/redirect"); + t.property(ClientProperties.FOLLOW_REDIRECTS, false); + assertEquals(303, t.request().get().getStatus()); + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/HttpHeadersTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/HttpHeadersTest.java new file mode 100644 index 0000000..438f4f1 --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/HttpHeadersTest.java
@@ -0,0 +1,69 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests the headers. + * + * @author Stepan Kopriva + */ +public class HttpHeadersTest extends JerseyTest{ + @Path("/test") + public static class HttpMethodResource { + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + return "POST"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(HttpHeadersTest.HttpMethodResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + @Test + public void testPost() { + Response response = target("test").request().header("X-CLIENT", "client").post(null); + + assertEquals(200, response.getStatus()); + assertTrue(response.hasEntity()); + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/NoEntityTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/NoEntityTest.java new file mode 100644 index 0000000..eacf6ed --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/NoEntityTest.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class NoEntityTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @GET + public Response get() { + return Response.status(Status.CONFLICT).build(); + } + + @POST + public void post(String entity) { + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + @Test + public void testGet() { + WebTarget r = target("test"); + + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testGetWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testPost() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + } + } + + @Test + public void testPostWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + cr.close(); + } + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/ParallelTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/ParallelTest.java new file mode 100644 index 0000000..8cd48ba --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/ParallelTest.java
@@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Assert; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests the parallel execution of multiple requests. + * + * @author Stepan Kopriva + */ +public class ParallelTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(ParallelTest.class.getName()); + + private static final int PARALLEL_CLIENTS = 10; + private static final String PATH = "test"; + private static final AtomicInteger receivedCounter = new AtomicInteger(0); + private static final AtomicInteger resourceCounter = new AtomicInteger(0); + private static final CyclicBarrier startBarrier = new CyclicBarrier(PARALLEL_CLIENTS + 1); + private static final CountDownLatch doneLatch = new CountDownLatch(PARALLEL_CLIENTS); + + @Path(PATH) + public static class MyResource { + + @GET + public String get() { + sleep(); + resourceCounter.addAndGet(1); + return "GET"; + } + + private void sleep() { + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + Logger.getLogger(ParallelTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + @Override + protected Application configure() { + return new ResourceConfig(ParallelTest.MyResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + @Test + public void testParallel() throws BrokenBarrierException, InterruptedException, TimeoutException { + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(PARALLEL_CLIENTS); + + try { + final WebTarget target = target(); + for (int i = 1; i <= PARALLEL_CLIENTS; i++) { + final int id = i; + executor.submit(new Runnable() { + @Override + public void run() { + try { + startBarrier.await(); + Response response; + response = target.path(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + receivedCounter.incrementAndGet(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + LOGGER.log(Level.WARNING, "Client thread " + id + " interrupted.", ex); + } catch (BrokenBarrierException ex) { + LOGGER.log(Level.INFO, "Client thread " + id + " failed on broken barrier.", ex); + } catch (Throwable t) { + t.printStackTrace(); + LOGGER.log(Level.WARNING, "Client thread " + id + " failed on unexpected exception.", t); + } finally { + doneLatch.countDown(); + } + } + }); + } + + startBarrier.await(1, TimeUnit.SECONDS); + + assertTrue("Waiting for clients to finish has timed out.", doneLatch.await(5 * getAsyncTimeoutMultiplier(), + TimeUnit.SECONDS)); + + assertEquals("Resource counter", PARALLEL_CLIENTS, resourceCounter.get()); + + assertEquals("Received counter", PARALLEL_CLIENTS, receivedCounter.get()); + } finally { + executor.shutdownNow(); + Assert.assertTrue("Executor termination", executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/TimeoutTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/TimeoutTest.java new file mode 100644 index 0000000..f920bd7 --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/TimeoutTest.java
@@ -0,0 +1,89 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Martin Matula + */ +public class TimeoutTest extends JerseyTest { + @Path("/test") + public static class TimeoutResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("timeout") + public String getTimeout() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "GET"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(TimeoutResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.READ_TIMEOUT, 1000); + config.connectorProvider(new GrizzlyConnectorProvider()); + } + + @Test + public void testFast() { + Response r = target("test").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testSlow() { + try { + target("test/timeout").request().get(); + fail("Timeout expected."); + } catch (ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(TimeoutException.class)); + } + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/TraceSupportTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/TraceSupportTest.java new file mode 100644 index 0000000..4a132ef --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/TraceSupportTest.java
@@ -0,0 +1,235 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This very basic resource showcases support of a HTTP TRACE method, + * not directly supported by JAX-RS API. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class TraceSupportTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName()); + + /** + * Programmatic tracing root resource path. + */ + public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic"; + + /** + * Annotated class-based tracing root resource path. + */ + public static final String ROOT_PATH_ANNOTATED = "tracing/annotated"; + + @HttpMethod(TRACE.NAME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TRACE { + public static final String NAME = "TRACE"; + } + + @Path(ROOT_PATH_ANNOTATED) + public static class TracingResource { + + @TRACE + @Produces("text/plain") + public String trace(Request request) { + return stringify((ContainerRequest) request); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TracingResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC); + resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector<ContainerRequestContext, Response>() { + + @Override + public Response apply(ContainerRequestContext request) { + if (request == null) { + return Response.noContent().build(); + } else { + return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build(); + } + } + }); + + return config.registerResources(resourceBuilder.build()); + + } + + private String[] expectedFragmentsProgrammatic = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic" + }; + private String[] expectedFragmentsAnnotated = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/annotated" + }; + + private WebTarget prepareTarget(String path) { + final WebTarget target = target(); + target.register(LoggingFeature.class); + return target.path(path); + } + + @Test + public void testProgrammaticApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsProgrammatic) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testAnnotatedApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsAnnotated) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testTraceWithEntity() throws Exception { + _testTraceWithEntity(false, false); + } + + @Test + public void testAsyncTraceWithEntity() throws Exception { + _testTraceWithEntity(true, false); + } + + @Test + public void testTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(false, true); + } + + @Test + public void testAsyncTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(true, true); + } + + private void _testTraceWithEntity(final boolean isAsync, final boolean useGrizzlyConnector) throws Exception { + try { + WebTarget target = useGrizzlyConnector ? createGrizzlyClient().target(target().getUri()) : target(); + target = target.path(ROOT_PATH_ANNOTATED); + + final Entity<String> entity = Entity.entity("trace", MediaType.WILDCARD_TYPE); + + Response response; + if (!isAsync) { + response = target.request().method(TRACE.NAME, entity); + } else { + response = target.request().async().method(TRACE.NAME, entity).get(); + } + + fail("A TRACE request MUST NOT include an entity. (response=" + response + ")"); + } catch (Exception e) { + // OK + } + } + + private Client createGrizzlyClient() { + return ClientBuilder.newClient(new ClientConfig().connectorProvider(new GrizzlyConnectorProvider())); + } + + + public static String stringify(ContainerRequest request) { + StringBuilder buffer = new StringBuilder(); + + printRequestLine(buffer, request); + printPrefixedHeaders(buffer, request.getHeaders()); + + if (request.hasEntity()) { + buffer.append(request.readEntity(String.class)).append("\n"); + } + + return buffer.toString(); + } + + private static void printRequestLine(StringBuilder buffer, ContainerRequest request) { + buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n"); + } + + private static void printPrefixedHeaders(StringBuilder buffer, Map<String, List<String>> headers) { + for (Map.Entry<String, List<String>> e : headers.entrySet()) { + List<String> val = e.getValue(); + String header = e.getKey(); + + if (val.size() == 1) { + buffer.append(header).append(": ").append(val.get(0)).append("\n"); + } else { + StringBuilder sb = new StringBuilder(); + boolean add = false; + for (String s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + buffer.append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } +}
diff --git a/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/UnderlyingHttpClientAccessTest.java b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/UnderlyingHttpClientAccessTest.java new file mode 100644 index 0000000..b756630 --- /dev/null +++ b/connectors/grizzly-connector/src/test/java/org/glassfish/jersey/grizzly/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.jersey.client.ClientConfig; + +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import com.ning.http.client.AsyncHttpClient; + +/** + * Test of access to the underlying HTTP client instance used by the connector. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class UnderlyingHttpClientAccessTest { + + /** + * Verifier of JERSEY-2424 fix. + */ + @Test + public void testHttpClientInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new GrizzlyConnectorProvider())); + final AsyncHttpClient hcOnClient = GrizzlyConnectorProvider.getHttpClient(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getHttpClient method above. + final WebTarget target = client.target("http://localhost/"); + final AsyncHttpClient hcOnTarget = GrizzlyConnectorProvider.getHttpClient(target); + + assertNotNull("HTTP client instance set on JerseyClient should not be null.", hcOnClient); + assertNotNull("HTTP client instance set on JerseyWebTarget should not be null.", hcOnTarget); + assertSame("HTTP client instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget" + + "(provided the target instance has not been further configured).", + hcOnClient, hcOnTarget + ); + } +}
diff --git a/connectors/jdk-connector/pom.xml b/connectors/jdk-connector/pom.xml new file mode 100644 index 0000000..ac16069 --- /dev/null +++ b/connectors/jdk-connector/pom.xml
@@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-jdk-connector</artifactId> + <packaging>jar</packaging> + <name>jersey-connectors-jdk</name> + + <description>Jersey Client Transport via JDK connector</description> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-grizzly2</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + </plugins> + </build> + +</project>
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/JdkConnectorProperties.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/JdkConnectorProperties.java new file mode 100644 index 0000000..7e602d0 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/JdkConnectorProperties.java
@@ -0,0 +1,148 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector; + +import java.net.CookiePolicy; +import java.util.Map; + +import org.glassfish.jersey.internal.util.PropertiesClass; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +/** + * Configuration options specific to {@link org.glassfish.jersey.jdk.connector.internal.JdkConnector}. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +@PropertiesClass +public final class JdkConnectorProperties { + + /** + * Configuration of the connector thread pool. + * <p/> + * An instance of {@link org.glassfish.jersey.jdk.connector.internal.ThreadPoolConfig} is expected. + */ + public static final String WORKER_THREAD_POOL_CONFIG = "jersey.config.client.JdkConnectorProvider.workerThreadPoolConfig"; + + /** + * Container idle timeout in milliseconds ({@link Integer} value). + * <p/> + * When the timeout elapses, the shared thread pool will be destroyed. + * <p/> + * The default value is {@value #DEFAULT_CONNECTION_CLOSE_WAIT} + */ + public static final String CONTAINER_IDLE_TIMEOUT = "jersey.config.client.JdkConnectorProvider.containerIdleTimeout"; + + /** + * A configurable property of HTTP parser. It defines the maximal acceptable size of HTTP response initial line, + * each header and chunk header. + * <p/> + * The default value is {@value #DEFAULT_MAX_HEADER_SIZE} + */ + public static final String MAX_HEADER_SIZE = "jersey.config.client.JdkConnectorProvider.maxHeaderSize"; + + /** + * The maximal number of redirects during single request. + * <p/> + * Value is expected to be positive {@link Integer}. Default value is {@value #DEFAULT_MAX_REDIRECTS}. + * <p/> + * HTTP redirection must be enabled by property {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}, + * otherwise {@code MAX_HEADER_SIZE} is not applied. + * + * @see org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS + * @see org.glassfish.jersey.jdk.connector.internal.RedirectException + */ + public static final String MAX_REDIRECTS = "jersey.config.client.JdkConnectorProvider.maxRedirects"; + + /** + * To set the cookie policy of this cookie manager. + * <p/> + * The default value is ACCEPT_ORIGINAL_SERVER. + * + * @see java.net.CookieManager + */ + public static final String COOKIE_POLICY = "jersey.config.client.JdkConnectorProvider.cookiePolicy"; + + /** + * A maximal number of open connection to each destination. A destination is determined by the following triple: + * <ul> + * <li>host</li> + * <li>port</li> + * <li>protocol (HTTP/HTTPS)</li> + * <ul/> + * <p/> + * The default value is {@value #DEFAULT_MAX_CONNECTIONS_PER_DESTINATION} + */ + public static final String MAX_CONNECTIONS_PER_DESTINATION = "jersey.config.client.JdkConnectorProvider" + + ".maxConnectionsPerDestination"; + + /** + * An amount of time in milliseconds ({@link Integer} value) during which an idle connection will be kept open. + * <p/> + * The default value is {@value #DEFAULT_CONNECTION_IDLE_TIMEOUT} + */ + public static final String CONNECTION_IDLE_TIMEOUT = "jersey.config.client.JdkConnectorProvider.connectionIdleTimeout"; + + /** + * Default value for the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property. + */ + public static final int DEFAULT_HTTP_CHUNK_SIZE = 4096; + + /** + * Default value for the {@link #MAX_HEADER_SIZE} property. + */ + public static final int DEFAULT_MAX_HEADER_SIZE = 8192; + + /** + * Default value for the {@link #MAX_REDIRECTS} property. + */ + public static final int DEFAULT_MAX_REDIRECTS = 5; + + /** + * Default value for the {@link #COOKIE_POLICY} property. + */ + public static final CookiePolicy DEFAULT_COOKIE_POLICY = CookiePolicy.ACCEPT_ORIGINAL_SERVER; + + /** + * Default value for the {@link #MAX_CONNECTIONS_PER_DESTINATION} property. + */ + public static final int DEFAULT_MAX_CONNECTIONS_PER_DESTINATION = 20; + + /** + * Default value for the {@link #CONNECTION_IDLE_TIMEOUT} property. + */ + public static final int DEFAULT_CONNECTION_IDLE_TIMEOUT = 1000000; + + /** + * Default value for the {@link #CONTAINER_IDLE_TIMEOUT} property. + */ + public static final int DEFAULT_CONNECTION_CLOSE_WAIT = 30_000; + + public static <T> T getValue(final Map<String, ?> properties, final String key, final Class<T> type) { + return PropertiesHelper.getValue(properties, key, type, null); + } + + public static <T> T getValue(final Map<String, ?> properties, final String key, T defaultValue, final Class<T> type) { + return PropertiesHelper.getValue(properties, key, defaultValue, type, null); + } + + /** + * Prevents instantiation. + */ + private JdkConnectorProperties() { + throw new AssertionError("No instances allowed."); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/JdkConnectorProvider.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/JdkConnectorProvider.java new file mode 100644 index 0000000..b205008 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/JdkConnectorProvider.java
@@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.jdk.connector.internal.JdkConnector; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class JdkConnectorProvider implements ConnectorProvider { + @Override + public Connector getConnector(Client client, Configuration config) { + return new JdkConnector(client, config); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyInputStream.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyInputStream.java new file mode 100644 index 0000000..236ac52 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyInputStream.java
@@ -0,0 +1,424 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Deque; +import java.util.LinkedList; +import java.util.concurrent.ExecutorService; + +import org.glassfish.jersey.internal.util.collection.ByteBufferInputStream; + +/** + * TODO Some of the operations added for async. support (e.g.) can be also supported in sync. mode + * <p/> + * Body stream that can operate either synchronously or asynchronously. See {@link BodyInputStream} for details. + * + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class AsynchronousBodyInputStream extends BodyInputStream { + + // marker of the end of data stream + private static final ByteBuffer EOF = ByteBuffer.wrap(new byte[] {}); + // marker of an error in the data stream + private static final ByteBuffer ERROR = ByteBuffer.wrap(new byte[] {}); + + // mode this stream operates in + private Mode mode = Mode.UNDECIDED; + private ReadListener readListener = null; + // read listener is not called always when data become available + // it must be called for the first time or after isReady returned false + private boolean callReadListener = false; + // exception stored until we come to ERROR marker in the input stream + private Throwable t = null; + // marker that the stream does not admit more data/errors/stream-end notifications + private boolean closedForInput = false; + // by default readListener is invoked on IO/worker threads + // this might deadlock the entire connector if a blocking operations are used inside the listener implementations + // the readListener will be invoked using this executor if present + private ExecutorService listenerExecutor = null; + // a listener used internally by the connector + private StateChangeLister stateChangeLister; + + // if in synchronous mode, this stream delegates to synchronousStream + private ByteBufferInputStream synchronousStream = null; + // data to be read + private Deque<ByteBuffer> data = new LinkedList<>(); + + synchronized void setListenerExecutor(ExecutorService listenerExecutor) { + assertAsynchronousOperation(); + this.listenerExecutor = listenerExecutor; + commitToMode(); + } + + @Override + public synchronized boolean isReady() { + assertAsynchronousOperation(); + + // return false if this stream has not been initialised + if (mode == Mode.UNDECIDED) { + return false; + } + + ByteBuffer headBuffer = data.peek(); + boolean ready = true; + + if (headBuffer == null) { + ready = false; + } + + if (headBuffer == ERROR) { + ready = false; + callOnError(t); + } + + if (headBuffer == EOF) { + ready = false; + callOnAllDataRead(); + } + + if (!ready) { + // returning false automatically enables listener + callReadListener = true; + } + + return ready; + } + + @Override + public synchronized void setReadListener(ReadListener readListener) { + if (this.readListener != null) { + throw new IllegalStateException(LocalizationMessages.READ_LISTENER_SET_ONLY_ONCE()); + } + + // make sure we are not already in synchronous mode + assertAsynchronousOperation(); + + this.readListener = readListener; + commitToMode(); + + // if there is an error or EOF at the head of the data queue, isReady will handle it + if (isReady()) { + callDataAvailable(); + } + } + + @Override + public int read() throws IOException { + commitToMode(); + + if (mode == Mode.SYNCHRONOUS) { + return synchronousStream.read(); + } + + validateState(); + return doRead(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + commitToMode(); + if (mode == Mode.SYNCHRONOUS) { + return synchronousStream.read(b, off, len); + } + + // some validation borrowed from InputStream + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + validateState(); + + for (int i = 0; i < len; i++) { + if (!hasDataToRead()) { + return i; + } + + b[off + i] = doRead(); + } + + // if we are here we were able to fill the entire buffer + return len; + } + + private synchronized byte doRead() { + // if we are here we passed all the validation, so there must be something to read + ByteBuffer headBuffer = data.peek(); + byte b = headBuffer.get(); + + if (!headBuffer.hasRemaining()) { + // remove empty buffer + data.poll(); + } + + return b; + } + + @Override + public int available() throws IOException { + commitToMode(); + // TODO this could be also supported in async mode + assertSynchronousOperation(); + return synchronousStream.available(); + } + + @Override + public long skip(long n) throws IOException { + commitToMode(); + // TODO this could be also supported in async mode + assertSynchronousOperation(); + return synchronousStream.skip(n); + } + + @Override + public int tryRead() throws IOException { + commitToMode(); + assertSynchronousOperation(); + return synchronousStream.tryRead(); + } + + @Override + public int tryRead(byte[] b) throws IOException { + commitToMode(); + assertSynchronousOperation(); + return synchronousStream.tryRead(b); + } + + @Override + public int tryRead(byte[] b, int off, int len) throws IOException { + commitToMode(); + assertSynchronousOperation(); + return synchronousStream.tryRead(b, off, len); + } + + synchronized void notifyDataAvailable(ByteBuffer availableData) { + assertClosedForInput(); + + if (!availableData.hasRemaining()) { + return; + } + + if (mode == Mode.SYNCHRONOUS) { + try { + synchronousStream.put(availableData); + } catch (InterruptedException e) { + synchronousStream.closeQueue(e); + } + return; + } + + data.add(availableData); + + if (readListener != null && callReadListener) { + callDataAvailable(); + } + } + + @Override + public void close() throws IOException { + if (mode == Mode.SYNCHRONOUS) { + synchronousStream.close(); + } + } + + synchronized void notifyError(Throwable t) { + assertClosedForInput(); + + if (stateChangeLister != null) { + stateChangeLister.onError(t); + } + + closedForInput = true; + + if (mode == Mode.SYNCHRONOUS) { + synchronousStream.closeQueue(t); + return; + } + + // we store the error and put a marker in the stream, so that the user can read all data that + // were successfully received up to the error. + this.t = t; + data.add(ERROR); + + if (mode == Mode.ASYNCHRONOUS && callReadListener) { + callOnError(t); + } + } + + synchronized void notifyAllDataRead() { + assertClosedForInput(); + + if (stateChangeLister != null) { + stateChangeLister.onAllDataRead(); + } + + if (mode == Mode.SYNCHRONOUS) { + synchronousStream.closeQueue(); + return; + } + + data.add(EOF); + + if (mode == Mode.ASYNCHRONOUS && callReadListener) { + callOnAllDataRead(); + } + } + + private synchronized void commitToMode() { + // return if the mode has already been committed + if (mode != Mode.UNDECIDED) { + return; + } + + // go asynchronous, if the user has made any move suggesting asynchronous mode + if (readListener != null || listenerExecutor != null) { + mode = Mode.ASYNCHRONOUS; + return; + } + + // go synchronous, if the user has not made any move suggesting asynchronous mode + mode = Mode.SYNCHRONOUS; + synchronousStream = new ByteBufferInputStream(); + // move all buffered data to synchronous stream + for (ByteBuffer b : data) { + if (b == EOF) { + synchronousStream.closeQueue(); + } else if (b == ERROR) { + synchronousStream.closeQueue(t); + } else { + try { + synchronousStream.put(b); + } catch (InterruptedException e) { + synchronousStream.closeQueue(e); + } + } + } + } + + private void assertAsynchronousOperation() { + if (mode == Mode.SYNCHRONOUS) { + throw new UnsupportedOperationException(LocalizationMessages.ASYNC_OPERATION_NOT_SUPPORTED()); + } + } + + private void assertSynchronousOperation() { + if (mode == Mode.ASYNCHRONOUS) { + throw new UnsupportedOperationException(LocalizationMessages.SYNC_OPERATION_NOT_SUPPORTED()); + } + } + + private void validateState() { + if (mode == Mode.ASYNCHRONOUS && !hasDataToRead()) { + throw new IllegalStateException(LocalizationMessages.WRITE_WHEN_NOT_READY()); + } + } + + private void assertClosedForInput() { + if (closedForInput) { + throw new IllegalStateException(LocalizationMessages.STREAM_CLOSED_FOR_INPUT()); + } + } + + private boolean hasDataToRead() { + ByteBuffer headBuffer = data.peek(); + if (headBuffer == null || headBuffer == EOF || headBuffer == ERROR || !headBuffer.hasRemaining()) { + return false; + } + + return true; + } + + private void callDataAvailable() { + callReadListener = false; + if (listenerExecutor == null) { + + try { + readListener.onDataAvailable(); + } catch (IOException e) { + readListener.onError(e); + } + } else { + listenerExecutor.submit(() -> { + try { + readListener.onDataAvailable(); + } catch (IOException e) { + readListener.onError(e); + } + }); + } + } + + private void callOnError(final Throwable t) { + if (listenerExecutor == null) { + readListener.onError(t); + } else { + listenerExecutor.submit(() -> readListener.onError(t)); + } + } + + private void callOnAllDataRead() { + if (listenerExecutor == null) { + try { + readListener.onAllDataRead(); + } catch (IOException e) { + readListener.onError(e); + } + } else { + listenerExecutor.submit(() -> { + try { + readListener.onAllDataRead(); + } catch (IOException e) { + readListener.onError(e); + } + }); + } + } + + synchronized void setStateChangeLister(StateChangeLister stateChangeLister) { + this.stateChangeLister = stateChangeLister; + + if (!data.isEmpty() && data.getLast() == EOF) { + stateChangeLister.onAllDataRead(); + } + + if (!data.isEmpty() && data.getLast() == ERROR) { + stateChangeLister.onError(t); + } + } + + private enum Mode { + SYNCHRONOUS, + ASYNCHRONOUS, + UNDECIDED + } + + /** + * Internal listener, so that the connection pool knows when the body has been read, + * so it can reuse/close the connection. + */ + interface StateChangeLister { + + void onError(Throwable t); + + void onAllDataRead(); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BodyInputStream.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BodyInputStream.java new file mode 100644 index 0000000..d379425 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BodyInputStream.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import org.glassfish.jersey.internal.util.collection.NonBlockingInputStream; + +/** + * TODO consider exposing the mode as part of the API, so the user can make decisions based on the mode + * <p/> + * An extension of {@link NonBlockingInputStream} that adds methods that enable using the stream asynchronously. + * The asynchronous mode is inspired by and works in a very similar way as Servlet asynchronous streams introduced in Servlet + * 3.1. + * <p/> + * The stream supports 2 modes of operation SYNCHRONOUS and ASYNCHRONOUS. + * The stream is one of the following 3 states: + * <ul> + * <li>UNDECIDED</li> + * <li>SYNCHRONOUS</li> + * <li>ASYNCHRONOUS</li> + * </ul> + * UNDECIDED is an initial mode and it commits either to SYNCHRONOUS or ASYNCHRONOUS. Once it commits to one of these + * 2 modes it cannot change to the other. The mode it commits to is decided based on the first use of te stream. + * If {@link #setReadListener(ReadListener)} is invoked before any of the read or tryRead methods, it commits to ASYNCHRONOUS + * mode and similarly if any of the read or tryRead methods is invoked before {@link #setReadListener(ReadListener)}, + * it commits to SYNCHRONOUS mode. + */ +abstract class BodyInputStream extends NonBlockingInputStream { + + /** + * Returns true if data can be read without blocking else returns + * false. + * <p/> + * If the stream is in ASYNCHRONOUS mode and the user attempts to read from it even though this method returns + * false, an {@link IllegalStateException} is thrown. + * + * @return <code>true</code> if data can be obtained without blocking, + * otherwise returns <code>false</code>. + */ + public abstract boolean isReady(); + + /** + * Instructs the stream to invoke the provided {@link ReadListener} when it is possible to read. + * <p/> + * If the stream is in UNDECIDED state, invoking this method will commit the stream to ASYNCHRONOUS mode. + * + * @param readListener the {@link ReadListener} that should be notified + * when it's possible to read. + * @throws IllegalStateException if one of the following conditions is true + * <ul> + * <li>the stream has already committed to SYNCHRONOUS mode. <li/> + * <li>setReadListener is called more than once within the scope of the same request. <li/> + * </ul> + * @throws NullPointerException if readListener is null + */ + public abstract void setReadListener(ReadListener readListener); +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BodyOutputStream.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BodyOutputStream.java new file mode 100644 index 0000000..a23551a --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BodyOutputStream.java
@@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.OutputStream; + +/** + * TODO consider exposing the mode as part of the API, so the user can make decisions based on the mode + * <p/> + * An extension of {@link OutputStream} that adds method that allow to use the stream asynchronously. + * It is inspired by and works in a very similar way as Servlet asynchronous streams introduced in Servlet 3.1. + * <p/> + * The stream supports 2 modes SYNCHRONOUS and ASYNCHRONOUS. + * The stream is one of the following 3 states: + * <ul> + * <li>UNDECIDED</li> + * <li>SYNCHRONOUS</li> + * <li>ASYNCHRONOUS</li> + * </ul> + * UNDECIDED is an initial mode and it commits either to SYNCHRONOUS or ASYNCHRONOUS. Once it commits to one of these + * 2 modes it cannot change to the other. The mode it commits to is decided based on the first use of the stream. + * If {@link #setWriteListener(WriteListener)} is invoked before any of the write methods, it commits to ASYNCHRONOUS + * mode and similarly if any of the write methods is invoked before {@link #setWriteListener(WriteListener)}, + * it commits to SYNCHRONOUS mode. + */ +abstract class BodyOutputStream extends OutputStream { + + /** + * Instructs the stream to invoke the provided {@link WriteListener} when it is possible to write. + * <p/> + * If the stream is in UNDECIDED state, invoking this method will commit the stream to ASYNCHRONOUS mode. + * + * @param writeListener the {@link WriteListener} that should be notified + * when it's possible to write. + * @throws IllegalStateException if one of the following conditions is true + * <ul> + * <li>the stream has already committed to SYNCHRONOUS mode. <li/> + * <li>setWriteListener is called more than once within the scope of the same request. <li/> + * </ul> + * @throws NullPointerException if writeListener is null + */ + public abstract void setWriteListener(WriteListener writeListener); + + /** + * Returns true if data can be written without blocking else returns + * false. + * <p/> + * If the stream is in ASYNCHRONOUS mode and the user attempts to write to it even though this method returns + * false, an {@link IllegalStateException} is thrown. + * + * @return <code>true</code> if data can be obtained without blocking, + * otherwise returns <code>false</code>. + */ + public abstract boolean isReady(); +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BufferedBodyOutputStream.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BufferedBodyOutputStream.java new file mode 100644 index 0000000..233258e --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/BufferedBodyOutputStream.java
@@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class BufferedBodyOutputStream extends BodyOutputStream { + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + /* This whole mode stuff is totally pointless if we buffer the request body, + it is here only in case someone complained that this stream does not behave as BodyOutputStream says it should */ + private volatile Mode mode = Mode.UNDECIDED; + + @Override + public void setWriteListener(WriteListener writeListener) { + if (mode == Mode.ASYNCHRONOUS) { + throw new IllegalStateException(LocalizationMessages.WRITE_LISTENER_SET_ONLY_ONCE()); + } + + if (mode == Mode.SYNCHRONOUS) { + throw new UnsupportedOperationException(LocalizationMessages.ASYNC_OPERATION_NOT_SUPPORTED()); + } + + mode = Mode.ASYNCHRONOUS; + try { + writeListener.onWritePossible(); + } catch (IOException e) { + writeListener.onError(e); + } + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void write(int b) throws IOException { + if (mode == Mode.UNDECIDED) { + mode = Mode.SYNCHRONOUS; + } + + buffer.write(b); + } + + ByteBuffer toBuffer() { + return ByteBuffer.wrap(buffer.toByteArray()); + } + + private enum Mode { + UNDECIDED, + ASYNCHRONOUS, + SYNCHRONOUS + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ChunkedBodyOutputStream.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ChunkedBodyOutputStream.java new file mode 100644 index 0000000..acf9c31 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ChunkedBodyOutputStream.java
@@ -0,0 +1,365 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Body stream that can operate either synchronously or asynchronously. See {@link BodyOutputStream} for details. + * + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class ChunkedBodyOutputStream extends BodyOutputStream { + + private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); + + private final int chunkSize; + private final int encodedFullChunkSize; + + // this stream is buffering by default; it has pending data up to dataBuffer.capacity() + private final ByteBuffer dataBuffer; + + // in sync. mode, the write operations will block until the stream is opened for data + private final CountDownLatch initialBlockingLatch = new CountDownLatch(1); + + private volatile Filter<ByteBuffer, ?, ?, ?> downstreamFilter; + private volatile WriteListener writeListener = null; + // an internal listener, so the connector can be notified when the stream has been closed (=body has been sent) + private volatile Listener closeListener; + // mode this stream operates in + private volatile Mode mode = Mode.UNDECIDED; + private volatile boolean ready = false; + // flag to make sure that a listener is called only for the first time or after isReady() returned false + private volatile boolean callListener = true; + + private volatile boolean closed = false; + + ChunkedBodyOutputStream(int chunkSize) { + this.chunkSize = chunkSize; + this.dataBuffer = ByteBuffer.allocate(chunkSize); + this.encodedFullChunkSize = HttpRequestEncoder.getChunkSize(chunkSize); + } + + @Override + public synchronized void setWriteListener(WriteListener writeListener) { + if (this.writeListener != null) { + throw new IllegalStateException(LocalizationMessages.WRITE_LISTENER_SET_ONLY_ONCE()); + } + + assertAsynchronousOperation(); + this.writeListener = writeListener; + commitToMode(); + + if (ready && callListener) { + callOnWritePossible(); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + commitToMode(); + + // input validation borrowed from OutputStream + if (b == null) { + throw new NullPointerException(); + } else if ((off < 0) || (off > b.length) || (len < 0) + || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + assertValidState(); + doInitialBlocking(); + + if (len < dataBuffer.remaining()) { + // if the data fit into the buffer, use write per byte + for (int i = off; i < off + len; i++) { + write(b[i]); + } + } else { + // if the data overflow the buffer, send a multiple of the buffer size and buffer the remainder + int currentDataLength = dataBuffer.position() + len; + int remainder = currentDataLength % dataBuffer.capacity(); + // buffer that will be send + ByteBuffer buffer = ByteBuffer.allocate(currentDataLength - remainder); + dataBuffer.flip(); + // put currently buffered data + buffer.put(dataBuffer); + // fill the rest with passed data + buffer.put(b, off, len - remainder); + buffer.flip(); + dataBuffer.clear(); + // buffer remaining data + dataBuffer.put(b, off + len - remainder, remainder); + // send the to-be-written buffer + write(buffer); + } + } + + @Override + public void flush() throws IOException { + super.flush(); + if (mode == Mode.UNDECIDED) { + // if we are not committed to any mode, any of the write operations has not been invoked yet + return; + } + + if (mode == Mode.ASYNCHRONOUS) { + assertValidState(); + } + + if (dataBuffer.position() == 0) { + // there is nothing buffered, so don't bother + return; + } + + dataBuffer.flip(); + write(dataBuffer); + } + + @Override + public void write(int b) throws IOException { + commitToMode(); + assertValidState(); + doInitialBlocking(); + + dataBuffer.put((byte) b); + if (!dataBuffer.hasRemaining()) { + // send the buffer if we have just filled it. + dataBuffer.flip(); + write(dataBuffer); + } + } + + @Override + public boolean isReady() { + // TODO we might support this in synchronous mode too + assertAsynchronousOperation(); + + if (!ready) { + callListener = true; + } + + return ready; + } + + private void assertValidState() { + if (closed) { + throw new IllegalStateException(LocalizationMessages.STREAM_CLOSED()); + } + + if (mode == Mode.ASYNCHRONOUS && !ready) { + // we are in asynchronous mode, but the user called write when the stream in non-ready state + throw new IllegalStateException(LocalizationMessages.WRITE_WHEN_NOT_READY()); + } + } + + protected void write(final ByteBuffer byteBuffer) throws IOException { + // do transport encoding on the raw data + ByteBuffer httpChunk = encodeToHttp(byteBuffer); + + if (mode == Mode.SYNCHRONOUS) { + final CountDownLatch writeLatch = new CountDownLatch(1); + final AtomicReference<Throwable> error = new AtomicReference<>(); + downstreamFilter.write(httpChunk, new CompletionHandler<ByteBuffer>() { + @Override + public void completed(ByteBuffer result) { + writeLatch.countDown(); + } + + @Override + public void failed(Throwable t) { + error.set(t); + writeLatch.countDown(); + } + }); + + try { + // block until the operation has completed + writeLatch.await(); + } catch (InterruptedException e) { + throw new IOException(LocalizationMessages.WRITING_FAILED(), e); + } + + byteBuffer.clear(); + + Throwable t = error.get(); + // check fo any errors + if (t != null) { + throw new IOException(LocalizationMessages.WRITING_FAILED(), t); + } + } else { + ready = false; + downstreamFilter.write(httpChunk, new CompletionHandler<ByteBuffer>() { + + @Override + public void completed(ByteBuffer result) { + ready = true; + byteBuffer.clear(); + if (callListener) { + callOnWritePossible(); + } + } + + @Override + public void failed(Throwable throwable) { + ready = false; + writeListener.onError(throwable); + } + }); + } + } + + synchronized void open(Filter<ByteBuffer, ?, ?, ?> downstreamFilter) { + this.downstreamFilter = downstreamFilter; + initialBlockingLatch.countDown(); + ready = true; + + if (mode == Mode.ASYNCHRONOUS && writeListener != null) { + callOnWritePossible(); + } + } + + protected void doInitialBlocking() throws IOException { + if (mode != Mode.SYNCHRONOUS || downstreamFilter != null) { + return; + } + + try { + initialBlockingLatch.await(); + } catch (InterruptedException e) { + throw new IOException(e); + } + } + + protected synchronized void commitToMode() { + // return if the mode has already been committed + if (mode != Mode.UNDECIDED) { + return; + } + + // go asynchronous, if the user has made any move suggesting asynchronous mode + if (writeListener != null) { + mode = Mode.ASYNCHRONOUS; + return; + } + + // go synchronous, if the user has not made any suggesting asynchronous mode + mode = Mode.SYNCHRONOUS; + } + + private void assertAsynchronousOperation() { + if (mode == Mode.SYNCHRONOUS) { + throw new UnsupportedOperationException(LocalizationMessages.ASYNC_OPERATION_NOT_SUPPORTED()); + } + } + + private void callOnWritePossible() { + callListener = false; + try { + writeListener.onWritePossible(); + } catch (IOException e) { + writeListener.onError(e); + } + } + + /** + * Set a close listener which will be called when the user closes the stream. + * <p/> + * This is used to indicate that the body has been completely written. + * + * @param closeListener close listener. + */ + synchronized void setCloseListener(Listener closeListener) { + this.closeListener = closeListener; + } + + /** + * Transform raw application data into HTTP body. + * + * @param byteBuffer application data. + * @return http body part. + */ + protected ByteBuffer encodeToHttp(ByteBuffer byteBuffer) { + // we expect the size of the buffer to be either a multiple of chunkSize + // or smaller than chunkSize in case of the last content-carrying chunk and closing chunk (the one sent by close()) + if (byteBuffer.remaining() < chunkSize) { + return HttpRequestEncoder.encodeChunk(byteBuffer); + } + + if (byteBuffer.remaining() % chunkSize != 0) { + // the buffer is neither a multiple of chunkSize nor smaller than chunkSize + throw new IllegalStateException(LocalizationMessages.BUFFER_INCORRECT_LENGTH()); + } + + int numberOfChunks = byteBuffer.remaining() / chunkSize; + ByteBuffer encodedChunks = ByteBuffer.allocate(numberOfChunks * encodedFullChunkSize); + + for (int i = 0; i < numberOfChunks; i++) { + byteBuffer.position(i * chunkSize); + byteBuffer.limit(i * chunkSize + chunkSize); + ByteBuffer encodeChunk = HttpRequestEncoder.encodeChunk(byteBuffer); + encodedChunks.put(encodeChunk); + } + + encodedChunks.flip(); + return encodedChunks; + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + + commitToMode(); + // just in case close is invoked without any data being written + doInitialBlocking(); + flush(); + // chunk-encoded message is finished with an empty chunk + write(EMPTY_BUFFER); + super.close(); + + closed = true; + synchronized (this) { + if (closeListener != null) { + closeListener.onClosed(); + } + } + } + + /** + * Set a close listener which will be called when the user closes the stream. + * <p/> + * This is used to indicate that the body has been completely written. + */ + interface Listener { + + void onClosed(); + } + + private enum Mode { + SYNCHRONOUS, + ASYNCHRONOUS, + UNDECIDED + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/CompletionHandler.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/CompletionHandler.java new file mode 100644 index 0000000..0297103 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/CompletionHandler.java
@@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +/** + * A callback to notify about asynchronous I/O operations status updates. + * + * @author Alexey Stashok + */ +abstract class CompletionHandler<E> { + + /** + * The operation has failed. + * + * @param throwable error, which occurred during operation execution. + */ + public void failed(Throwable throwable) { + } + + /** + * The operation has completed. + * + * @param result the operation result. + */ + public void completed(E result) { + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java new file mode 100644 index 0000000..63a571c --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java
@@ -0,0 +1,246 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.CookiePolicy; +import java.net.URI; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; + +/** + * A container for connector configuration to make it easier to move around. + * <p/> + * This is internal to the connector and is not used by the user for configuration. + * + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class ConnectorConfiguration { + + private static final Logger LOGGER = Logger.getLogger(ConnectorConfiguration.class.getName()); + + private final int chunkSize; + private final boolean followRedirects; + private final int maxRedirects; + private final ThreadPoolConfig threadPoolConfig; + private final int containerIdleTimeout; + private final int maxHeaderSize; + private final CookiePolicy cookiePolicy; + private final int maxConnectionsPerDestination; + private final int connectionIdleTimeout; + private final SSLContext sslContext; + private final HostnameVerifier hostnameVerifier; + private final int responseTimeout; + private final int connectTimeout; + private final ProxyConfiguration proxyConfiguration; + + ConnectorConfiguration(Client client, Configuration config) { + final Map<String, Object> properties = config.getProperties(); + + int proposedChunkSize = JdkConnectorProperties.getValue(properties, ClientProperties.CHUNKED_ENCODING_SIZE, + JdkConnectorProperties.DEFAULT_HTTP_CHUNK_SIZE, Integer.class); + if (proposedChunkSize < 0) { + LOGGER.warning(LocalizationMessages.NEGATIVE_CHUNK_SIZE(proposedChunkSize, + JdkConnectorProperties.DEFAULT_HTTP_CHUNK_SIZE)); + proposedChunkSize = JdkConnectorProperties.DEFAULT_HTTP_CHUNK_SIZE; + } + + chunkSize = proposedChunkSize; + + threadPoolConfig = JdkConnectorProperties.getValue(properties, JdkConnectorProperties.WORKER_THREAD_POOL_CONFIG, + ThreadPoolConfig.defaultConfig(), ThreadPoolConfig.class); + threadPoolConfig.setCorePoolSize(ClientProperties.getValue(properties, ClientProperties.ASYNC_THREADPOOL_SIZE, + threadPoolConfig.getCorePoolSize(), Integer.class)); + containerIdleTimeout = JdkConnectorProperties.getValue(properties, JdkConnectorProperties.CONTAINER_IDLE_TIMEOUT, + JdkConnectorProperties.DEFAULT_CONNECTION_CLOSE_WAIT, Integer.class); + + maxHeaderSize = JdkConnectorProperties.getValue(properties, JdkConnectorProperties.MAX_HEADER_SIZE, + JdkConnectorProperties.DEFAULT_MAX_HEADER_SIZE, Integer.class); + followRedirects = ClientProperties.getValue(properties, ClientProperties.FOLLOW_REDIRECTS, true, Boolean.class); + + cookiePolicy = JdkConnectorProperties.getValue(properties, JdkConnectorProperties.COOKIE_POLICY, + JdkConnectorProperties.DEFAULT_COOKIE_POLICY, CookiePolicy.class); + maxRedirects = JdkConnectorProperties.getValue(properties, JdkConnectorProperties.MAX_REDIRECTS, + JdkConnectorProperties.DEFAULT_MAX_REDIRECTS, Integer.class); + + maxConnectionsPerDestination = JdkConnectorProperties.getValue(properties, + JdkConnectorProperties.MAX_CONNECTIONS_PER_DESTINATION, + JdkConnectorProperties.DEFAULT_MAX_CONNECTIONS_PER_DESTINATION, Integer.class); + + connectionIdleTimeout = JdkConnectorProperties + .getValue(properties, JdkConnectorProperties.CONNECTION_IDLE_TIMEOUT, + JdkConnectorProperties.DEFAULT_CONNECTION_IDLE_TIMEOUT, Integer.class); + + responseTimeout = ClientProperties.getValue(properties, ClientProperties.READ_TIMEOUT, 0, Integer.class); + + connectTimeout = ClientProperties.getValue(properties, ClientProperties.CONNECT_TIMEOUT, 0, Integer.class); + + if (client.getSslContext() == null) { + sslContext = SslConfigurator.getDefaultContext(); + } else { + sslContext = client.getSslContext(); + } + + hostnameVerifier = client.getHostnameVerifier(); + + proxyConfiguration = new ProxyConfiguration(properties); + + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, LocalizationMessages.CONNECTOR_CONFIGURATION(toString())); + } + } + + int getChunkSize() { + return chunkSize; + } + + boolean getFollowRedirects() { + return followRedirects; + } + + int getMaxRedirects() { + return maxRedirects; + } + + ThreadPoolConfig getThreadPoolConfig() { + return threadPoolConfig; + } + + int getContainerIdleTimeout() { + return containerIdleTimeout; + } + + int getMaxHeaderSize() { + return maxHeaderSize; + } + + CookiePolicy getCookiePolicy() { + return cookiePolicy; + } + + int getMaxConnectionsPerDestination() { + return maxConnectionsPerDestination; + } + + int getConnectionIdleTimeout() { + return connectionIdleTimeout; + } + + SSLContext getSslContext() { + return sslContext; + } + + HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + int getResponseTimeout() { + return responseTimeout; + } + + int getConnectTimeout() { + return connectTimeout; + } + + public ProxyConfiguration getProxyConfiguration() { + return proxyConfiguration; + } + + @Override + public String toString() { + return "ConnectorConfiguration{" + + ", chunkSize=" + chunkSize + + ", followRedirects=" + followRedirects + + ", maxRedirects=" + maxRedirects + + ", threadPoolConfig=" + threadPoolConfig + + ", containerIdleTimeout=" + containerIdleTimeout + + ", maxHeaderSize=" + maxHeaderSize + + ", cookiePolicy=" + cookiePolicy + + ", maxConnectionsPerDestination=" + maxConnectionsPerDestination + + ", connectionIdleTimeout=" + connectionIdleTimeout + + ", sslContext=" + sslContext + + ", hostnameVerifier=" + hostnameVerifier + + ", responseTimeout=" + responseTimeout + + ", connectTimeout=" + connectTimeout + + ", proxyConfiguration=" + proxyConfiguration.toString() + + '}'; + } + + static class ProxyConfiguration { + + private final boolean configured; + private final String host; + private final int port; + private final String userName; + private final String password; + + private ProxyConfiguration(Map<String, Object> properties) { + String uriStr = ClientProperties.getValue(properties, ClientProperties.PROXY_URI, String.class); + if (uriStr == null) { + configured = false; + host = null; + port = -1; + userName = null; + password = null; + return; + } + + configured = true; + URI proxyUri = URI.create(uriStr); + host = proxyUri.getHost(); + + if (proxyUri.getPort() == -1) { + port = 8080; + } else { + port = proxyUri.getPort(); + } + + userName = JdkConnectorProperties.getValue(properties, ClientProperties.PROXY_USERNAME, String.class); + password = JdkConnectorProperties.getValue(properties, ClientProperties.PROXY_PASSWORD, String.class); + } + + boolean isConfigured() { + return configured; + } + + String getHost() { + return host; + } + + int getPort() { + return port; + } + + String getUserName() { + return userName; + } + + String getPassword() { + return password; + } + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Constants.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Constants.java new file mode 100644 index 0000000..9b3b8e3 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Constants.java
@@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class Constants { + + static final String CONNECTION = "Connection"; + static final String CONNECTION_CLOSE = "Close"; + static final String HTTPS = "https"; + static String TRANSFER_ENCODING_HEADER = "Transfer-Encoding"; + static String TRANSFER_ENCODING_CHUNKED = "chunked"; + static String CONTENT_LENGTH = "Content-Length"; + static String HOST = "Host"; + static final String HEAD = "HEAD"; + static final String CONNECT = "CONNECT"; + static final String GET = "GET"; + static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; + static final String PROXY_CONNECTION = "ProxyConnection"; + static final String KEEP_ALIVE = "keep-alive"; + /** + * Basic authentication scheme key. + */ + static final String BASIC = "Basic"; + + /** + * Digest authentication scheme key. + */ + static final String DIGEST = "Digest"; +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/DestinationConnectionPool.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/DestinationConnectionPool.java new file mode 100644 index 0000000..26a9aed --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/DestinationConnectionPool.java
@@ -0,0 +1,451 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ScheduledExecutorService; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class DestinationConnectionPool { + + private final ConnectorConfiguration configuration; + private final Queue<HttpConnection> idleConnections = new ConcurrentLinkedDeque<>(); + private final Set<HttpConnection> connections = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Queue<RequestRecord> pendingRequests = new ConcurrentLinkedDeque<>(); + private final Map<HttpConnection, RequestRecord> requestsInProgress = new HashMap<>(); + private final CookieManager cookieManager; + private final ScheduledExecutorService scheduler; + private final ConnectionStateListener connectionStateListener; + + private volatile ConnectionCloseListener connectionCloseListener; + + private int connectionCounter = 0; + private boolean closed = false; + + DestinationConnectionPool(ConnectorConfiguration configuration, + CookieManager cookieManager, + ScheduledExecutorService scheduler) { + this.configuration = configuration; + this.cookieManager = cookieManager; + this.scheduler = scheduler; + this.connectionStateListener = new ConnectionStateListener(); + } + + void setConnectionCloseListener(ConnectionCloseListener connectionCloseListener) { + this.connectionCloseListener = connectionCloseListener; + } + + void send(HttpRequest httpRequest, CompletionHandler<HttpResponse> completionHandler) { + pendingRequests.add(new RequestRecord(httpRequest, completionHandler)); + processPendingRequests(); + } + + private void processPendingRequests(HttpConnection connection) { + HttpRequest httpRequest; + CompletionHandler<HttpResponse> completionHandler; + + synchronized (this) { + /* this is synchronized so that another thread does not steal the pending request at the head of the queue + while we investigate if we can execute it. */ + RequestRecord pendingHead = pendingRequests.poll(); + if (pendingHead == null) { + + idleConnections.add(connection); + + // no pending requests + return; + } + + httpRequest = pendingHead.request; + completionHandler = pendingHead.completionHandler; + } + + // if there was a connection available just use it + requestsInProgress.put(connection, new RequestRecord(httpRequest, completionHandler)); + connection.send(httpRequest); + } + + private void processPendingRequests() { + HttpConnection connection; + HttpRequest httpRequest; + CompletionHandler<HttpResponse> completionHandler; + + synchronized (this) { + /* this is synchronized so that another thread does not steal the pending request at the head of the queue + while we investigate if we can execute it. */ + RequestRecord pendingHead = pendingRequests.peek(); + if (pendingHead == null) { + // no pending requests + return; + } + + httpRequest = pendingHead.request; + completionHandler = pendingHead.completionHandler; + + connection = idleConnections.poll(); + if (connection != null) { + pendingRequests.poll(); + } + } + + if (connection != null) { + // if there was a connection available just use it + requestsInProgress.put(connection, new RequestRecord(httpRequest, completionHandler)); + connection.send(httpRequest); + return; + } + + // if there was not a connection available keep this requests in pending list and try to create a connection + synchronized (this) { + // synchronized because other thread might open/close connections, so we have to make sure we get the limits right. + + if (configuration.getMaxConnectionsPerDestination() == connectionCounter) { + // we are at the limit for this destination, just wait for a connection to become idle or close + return; + } + + // create a connection + connection = new HttpConnection(httpRequest.getUri(), cookieManager, configuration, scheduler, + connectionStateListener); + connections.add(connection); + connectionCounter++; + } + + // we don't want to connect inside the synchronized block + connection.connect(); + } + + synchronized void close() { + if (closed) { + return; + } + + closed = true; + + connections.forEach(HttpConnection::close); + } + + private RequestRecord getRequest(HttpConnection connection) { + RequestRecord requestRecord = requestsInProgress.get(connection); + if (requestRecord == null) { + throw new IllegalStateException("Request not found"); + } + + return requestRecord; + } + + private RequestRecord removeRequest(HttpConnection connection) { + RequestRecord requestRecord = requestsInProgress.get(connection); + if (requestRecord == null) { + throw new IllegalStateException("Request not found"); + } + + return requestRecord; + } + + private void cleanClosedConnection(HttpConnection connection) { + if (closed) { + return; + } + + RequestRecord pendingRequest; + synchronized (this) { + idleConnections.remove(connection); + connections.remove(connection); + connectionCounter--; + + pendingRequest = pendingRequests.peek(); + if (pendingRequest == null) { + if (connectionCounter == 0) { + connectionCloseListener.onLastConnectionClosed(); + } + return; + } + } + + processPendingRequests(); + } + + private void handleIllegalStateTransition(HttpConnection.State oldState, HttpConnection.State newState) { + throw new IllegalStateException("Illegal state transition, old state: " + oldState + " new state: " + newState); + } + + private synchronized void removeAllPendingWithError(Throwable t) { + for (RequestRecord requestRecord : pendingRequests) { + requestRecord.completionHandler.failed(t); + } + + pendingRequests.clear(); + } + + private class ConnectionStateListener implements HttpConnection.StateChangeListener { + + @Override + public void onStateChanged(HttpConnection connection, HttpConnection.State oldState, HttpConnection.State newState) { + switch (newState) { + + case IDLE: { + switch (oldState) { + case RECEIVED: + case CONNECTING: { + processPendingRequests(connection); + return; + } + + default: { + handleIllegalStateTransition(oldState, newState); + return; + } + } + } + + case RECEIVED: { + switch (oldState) { + case RECEIVING_HEADER: { + RequestRecord request = removeRequest(connection); + request.completionHandler.completed(connection.getHttResponse()); + return; + } + + case RECEIVING_BODY: { + removeRequest(connection); + return; + } + + default: { + handleIllegalStateTransition(oldState, newState); + return; + } + } + } + + case RECEIVING_BODY: { + switch (oldState) { + case RECEIVING_HEADER: { + RequestRecord request = getRequest(connection); + request.response = connection.getHttResponse(); + request.completionHandler.completed(connection.getHttResponse()); + return; + } + + default: { + handleIllegalStateTransition(oldState, newState); + return; + } + } + } + + case ERROR: { + switch (oldState) { + case SENDING_REQUEST: { + RequestRecord request = removeRequest(connection); + request.completionHandler.failed(connection.getError()); + return; + } + + case RECEIVING_HEADER: { + RequestRecord request = removeRequest(connection); + request.completionHandler.failed(connection.getError()); + return; + } + + case RECEIVING_BODY: { + requestsInProgress.remove(connection); + return; + } + + case CONNECTING: { + removeAllPendingWithError(connection.getError()); + return; + } + + default: { + connection.getError().printStackTrace(); + handleIllegalStateTransition(oldState, newState); + return; + } + } + } + + case RESPONSE_TIMEOUT: { + switch (oldState) { + case RECEIVING_HEADER: { + RequestRecord request = removeRequest(connection); + request.completionHandler + .failed(new IOException(LocalizationMessages.TIMEOUT_RECEIVING_RESPONSE())); + return; + } + + case RECEIVING_BODY: { + RequestRecord request = requestsInProgress.remove(connection); + request.response.getBodyStream() + .notifyError(new IOException(LocalizationMessages.TIMEOUT_RECEIVING_RESPONSE_BODY())); + return; + } + + default: { + handleIllegalStateTransition(oldState, newState); + return; + } + } + } + + case CLOSED_BY_SERVER: { + switch (oldState) { + case SENDING_REQUEST: { + RequestRecord request = removeRequest(connection); + request.completionHandler + .failed(new IOException(LocalizationMessages.CLOSED_WHILE_SENDING_REQUEST())); + return; + } + + case RECEIVING_HEADER: { + RequestRecord request = removeRequest(connection); + request.completionHandler + .failed(new IOException(LocalizationMessages.CLOSED_WHILE_RECEIVING_RESPONSE(), + connection.getError())); + return; + } + + case RECEIVING_BODY: { + RequestRecord request = requestsInProgress.remove(connection); + request.response.getBodyStream().notifyError( + new IOException(LocalizationMessages.CLOSED_WHILE_RECEIVING_BODY(), + connection.getError())); + return; + } + + case CONNECTING: { + removeAllPendingWithError(new IOException(LocalizationMessages.CONNECTION_CLOSED())); + return; + } + } + } + + case CLOSED: { + switch (oldState) { + case SENDING_REQUEST: { + RequestRecord request = removeRequest(connection); + request.completionHandler + .failed(new IOException(LocalizationMessages.CLOSED_BY_CLIENT_WHILE_SENDING())); + cleanClosedConnection(connection); + return; + } + + case RECEIVING_HEADER: { + RequestRecord request = removeRequest(connection); + request.completionHandler + .failed(new IOException(LocalizationMessages.CLOSED_WHILE_RECEIVING_RESPONSE())); + cleanClosedConnection(connection); + return; + } + + case RECEIVING_BODY: { + RequestRecord request = requestsInProgress.remove(connection); + request.response.getBodyStream().notifyError( + new IOException(LocalizationMessages.CLOSED_BY_CLIENT_WHILE_RECEIVING_BODY(), + connection.getError())); + cleanClosedConnection(connection); + return; + } + + default: { + cleanClosedConnection(connection); + return; + } + } + } + + case CONNECT_TIMEOUT: { + switch (oldState) { + case CONNECTING: { + removeAllPendingWithError(new IOException(LocalizationMessages.CONNECTION_TIMEOUT())); + return; + } + + default: { + cleanClosedConnection(connection); + } + } + } + } + } + } + + private static class RequestRecord { + + private final HttpRequest request; + private final CompletionHandler<HttpResponse> completionHandler; + private HttpResponse response; + + RequestRecord(HttpRequest request, CompletionHandler<HttpResponse> completionHandler) { + this.request = request; + this.completionHandler = completionHandler; + } + } + + static class DestinationKey { + + private final String host; + private final int port; + private final boolean secure; + + DestinationKey(URI uri) { + host = uri.getHost(); + port = Utils.getPort(uri); + secure = Constants.HTTPS.equalsIgnoreCase(uri.getScheme()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DestinationKey that = (DestinationKey) o; + + return port == that.port && secure == that.secure && host.equals(that.host); + } + + @Override + public int hashCode() { + int result = host.hashCode(); + result = 31 * result + port; + result = 31 * result + (secure ? 1 : 0); + return result; + } + } + + interface ConnectionCloseListener { + + void onLastConnectionClosed(); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Filter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Filter.java new file mode 100644 index 0000000..f42e68a --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Filter.java
@@ -0,0 +1,226 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.SocketAddress; + +/** + * A filter can add functionality to JDK client transport. Filters are composed together to + * create JDK client transport. + * + * @author Petr Janouch (petr.janouch at oracle.com) + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class Filter<UP_IN, UP_OUT, DOWN_OUT, DOWN_IN> { + + protected volatile Filter<?, ?, UP_IN, UP_OUT> upstreamFilter = null; + protected final Filter<DOWN_OUT, DOWN_IN, ?, ?> downstreamFilter; + + /** + * Constructor. + * + * @param downstreamFilter downstream filter. Accessible directly as {@link #downstreamFilter} protected field. + */ + Filter(Filter<DOWN_OUT, DOWN_IN, ?, ?> downstreamFilter) { + this.downstreamFilter = downstreamFilter; + } + + /** + * Perform write operation for this filter and invokes write method on the next filter in the filter chain. + * + * @param data on which write operation is performed. + * @param completionHandler will be invoked when the write operation is completed or has failed. + */ + void write(UP_IN data, CompletionHandler<UP_IN> completionHandler) { + } + + /** + * Close the filter, invokes close operation on the next filter in the filter chain. + * <p/> + * The filter is expected to clean up any allocated resources and pass the invocation to downstream filter. + */ + void close() { + if (downstreamFilter != null) { + downstreamFilter.close(); + } + } + + /** + * Signal to turn on SSL, it is passed on in the filter chain until a filter responsible for SSL is reached. + */ + void startSsl() { + if (downstreamFilter != null) { + downstreamFilter.startSsl(); + } + } + + /** + * Initiate connect. + * <p/> + * If the {@link Filter} needs to do something during this phase, it must implement {@link + * #handleConnect(SocketAddress, Filter)} + * method. + * + * @param address an address where to connect (server or proxy). + * @param upstreamFilter a filter positioned upstream. + */ + void connect(SocketAddress address, Filter<?, ?, UP_IN, UP_OUT> upstreamFilter) { + this.upstreamFilter = upstreamFilter; + + handleConnect(address, upstreamFilter); + + if (downstreamFilter != null) { + downstreamFilter.connect(address, this); + } + } + + /** + * An event listener that is called when a connection is set up. + * This event travels up in the filter chain. + * <p/> + * If the {@link Filter} needs to process this event, it must implement {@link #processConnect()} method. + */ + void onConnect() { + processConnect(); + + if (upstreamFilter != null) { + upstreamFilter.onConnect(); + } + } + + /** + * An event listener that is called when some data is read. + * <p/> + * If the {@link Filter} needs to process this event, it must implement {@link #onRead(Object)} ()} method. + * If the method returns {@code true}, the processing will continue with upstream filters; if the method invocation + * returns {@code false}, the processing won't continue. + * + * @param data that has been read. + */ + @SuppressWarnings("unchecked") + final void onRead(DOWN_IN data) { + if (processRead(data)) { + if (upstreamFilter != null) { + UP_OUT _data; + try { + _data = (UP_OUT) data; + } catch (Exception e) { + throw new IllegalStateException("Cannot pass message of different type from filter input to filter output"); + } + upstreamFilter.onRead(_data); + } + } + } + + /** + * An event listener that is called when the connection is closed by the peer. + * <p/> + * If the {@link Filter} needs to process this event, it must implement {@link #processConnectionClosed()} method. + */ + final void onConnectionClosed() { + processConnectionClosed(); + + if (upstreamFilter != null) { + upstreamFilter.onConnectionClosed(); + } + } + + /** + * An event listener that is called, when SSL completes its handshake. + * <p/> + * If the {@link Filter} needs to process this event, it must implement {@link #processSslHandshakeCompleted()} method. + */ + final void onSslHandshakeCompleted() { + processSslHandshakeCompleted(); + + if (upstreamFilter != null) { + upstreamFilter.onSslHandshakeCompleted(); + } + } + + /** + * An event listener that is called when an error has occurred. + * <p/> + * Errors travel in direction from downstream filter to upstream filter. + * <p/> + * If the {@link Filter} needs to process this event, it must implement {@link #processError(Throwable)} method. + * + * @param t an error that has occurred. + */ + final void onError(Throwable t) { + processError(t); + + if (upstreamFilter != null) { + upstreamFilter.onError(t); + } + } + + /** + * Handle {@link #connect(SocketAddress, Filter)}. + * + * @param address an address where to connect (server or proxy). + * @param upstreamFilter a filter positioned upstream. + * @see #connect(SocketAddress, Filter) + */ + void handleConnect(SocketAddress address, Filter upstreamFilter) { + } + + /** + * Process {@link #onConnect()}. + * + * @see #onConnect() + */ + void processConnect() { + } + + /** + * Process {@link #onRead(Object)}. + * + * @param data read data. + * @return {@code true} if the data should be sent to processing to upper filter in the chain, {@code false} otherwise. + * @see #onRead(Object). + */ + boolean processRead(DOWN_IN data) { + return true; + } + + /** + * Process {@link #onConnectionClosed()}. + * + * @see #onConnectionClosed() + */ + void processConnectionClosed() { + } + + /** + * Process {@link #onSslHandshakeCompleted()}. + * + * @see #onSslHandshakeCompleted() + */ + void processSslHandshakeCompleted() { + } + + /** + * Process {@link #onError(Throwable)}. + * + * @param t an error that has occurred. + * @see #onError(Throwable) + */ + void processError(Throwable t) { + } +} +
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java new file mode 100644 index 0000000..ed6c3b4 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java
@@ -0,0 +1,453 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpConnection { + + /** + * Input buffer that is used by {@link TransportFilter} when SSL is turned on. + * The size cannot be smaller than a maximal size of a SSL packet, which is 16kB for payload + header, because + * {@link SslFilter} does not have its own buffer for buffering incoming + * data and therefore the entire SSL packet must fit into {@link SslFilter} + * input buffer. + * <p/> + */ + private static final int SSL_INPUT_BUFFER_SIZE = 17_000; + /** + * Input buffer that is used by {@link TransportFilter} when SSL is not turned on. + */ + private static final int INPUT_BUFFER_SIZE = 2048; + + private static final Logger LOGGER = Logger.getLogger(HttpConnection.class.getName()); + + private final Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> filterChain; + private final CookieManager cookieManager; + // we are interested only in host-port pair, but URI is a convenient holder for it + private final URI uri; + private final StateChangeListener stateListener; + private final ScheduledExecutorService scheduler; + private final ConnectorConfiguration configuration; + + private HttpRequest httpRequest; + private HttpResponse httResponse; + private Throwable error; + volatile State state = State.CREATED; + + // by default we treat all connection as persistent + // this flag will change to false if we receive "Connection: Close" header + private boolean persistentConnection = true; + + private Future<?> responseTimeout; + private Future<?> idleTimeout; + private Future<?> connectTimeout; + + HttpConnection(URI uri, + CookieManager cookieManager, + ConnectorConfiguration configuration, + ScheduledExecutorService scheduler, + StateChangeListener stateListener) { + this.uri = uri; + this.cookieManager = cookieManager; + this.stateListener = stateListener; + this.configuration = configuration; + this.scheduler = scheduler; + filterChain = createFilterChain(uri, configuration); + } + + synchronized void connect() { + if (state != State.CREATED) { + throw new IllegalStateException(LocalizationMessages.HTTP_CONNECTION_ESTABLISHING_ILLEGAL_STATE(state)); + } + changeState(State.CONNECTING); + scheduleConnectTimeout(); + filterChain.connect(new InetSocketAddress(uri.getHost(), Utils.getPort(uri)), null); + } + + synchronized void send(final HttpRequest httpRequest) { + if (state != State.IDLE) { + throw new IllegalStateException( + "Http request cannot be sent over a connection that is in other state than IDLE. Current state: " + state); + } + + cancelIdleTimeout(); + + this.httpRequest = httpRequest; + // clean state left by previous request + httResponse = null; + error = null; + persistentConnection = true; + changeState(State.SENDING_REQUEST); + + addRequestHeaders(); + + filterChain.write(httpRequest, new CompletionHandler<HttpRequest>() { + @Override + public void failed(Throwable throwable) { + handleError(throwable); + } + + @Override + public void completed(HttpRequest result) { + handleHeaderSent(); + } + }); + } + + synchronized void close() { + if (state == State.CLOSED) { + return; + } + + cancelAllTimeouts(); + filterChain.close(); + changeState(State.CLOSED); + } + + private synchronized void handleHeaderSent() { + if (state != State.SENDING_REQUEST) { + return; + } + + scheduleResponseTimeout(); + + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.NONE + || httpRequest.getBodyMode() == HttpRequest.BodyMode.BUFFERED) { + changeState(State.RECEIVING_HEADER); + } else { + ChunkedBodyOutputStream bodyStream = (ChunkedBodyOutputStream) httpRequest.getBodyStream(); + bodyStream.setCloseListener(() -> { + synchronized (HttpConnection.this) { + if (state != State.SENDING_REQUEST) { + return; + } + } + changeState(State.RECEIVING_HEADER); + }); + } + } + + private void addRequestHeaders() { + Map<String, List<String>> cookies; + try { + cookies = cookieManager.get(httpRequest.getUri(), httpRequest.getHeaders()); + } catch (IOException e) { + handleError(e); + return; + } + + // unfortunately CookieManager returns ""Cookie" -> empty list" pair if the cookie is not set + cookies.entrySet().stream().filter(cookieHeader -> cookieHeader.getValue() != null && !cookieHeader.getValue().isEmpty()) + .forEach(cookieHeader -> httpRequest.getHeaders().put(cookieHeader.getKey(), cookieHeader.getValue())); + } + + private void processResponseHeaders(HttpResponse response) throws IOException { + cookieManager.put(httpRequest.getUri(), httResponse.getHeaders()); + List<String> connectionValues = response.getHeader(Constants.CONNECTION); + if (connectionValues != null) { + connectionValues.stream().filter(connectionValue -> connectionValue.equalsIgnoreCase(Constants.CONNECTION_CLOSE)) + .forEach(connectionValue -> persistentConnection = false); + } + } + + protected Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> createFilterChain(URI uri, + ConnectorConfiguration + configuration) { + boolean secure = Constants.HTTPS.equals(uri.getScheme()); + Filter<ByteBuffer, ByteBuffer, ?, ?> socket; + + if (secure) { + SSLContext sslContext = configuration.getSslContext(); + TransportFilter transportFilter = new TransportFilter(SSL_INPUT_BUFFER_SIZE, configuration.getThreadPoolConfig(), + configuration.getContainerIdleTimeout()); + + if (sslContext == null) { + sslContext = SslConfigurator.getDefaultContext(); + + } + + socket = new SslFilter(transportFilter, sslContext, uri.getHost(), configuration.getHostnameVerifier()); + } else { + socket = new TransportFilter(INPUT_BUFFER_SIZE, configuration.getThreadPoolConfig(), + configuration.getContainerIdleTimeout()); + } + + int maxHeaderSize = configuration.getMaxHeaderSize(); + HttpFilter httpFilter = new HttpFilter(socket, maxHeaderSize, maxHeaderSize + INPUT_BUFFER_SIZE); + + ConnectorConfiguration.ProxyConfiguration proxyConfiguration = configuration.getProxyConfiguration(); + if (proxyConfiguration.isConfigured()) { + ProxyFilter proxyFilter = new ProxyFilter(httpFilter, proxyConfiguration); + return new ConnectionFilter(proxyFilter); + } + + return new ConnectionFilter(httpFilter); + } + + private void changeState(State newState) { + State old = state; + state = newState; + + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.finest(LocalizationMessages.CONNECTION_CHANGING_STATE(uri.getHost(), uri.getPort(), old, newState)); + } + + stateListener.onStateChanged(this, old, newState); + } + + private void scheduleResponseTimeout() { + if (configuration.getResponseTimeout() == 0) { + return; + } + + responseTimeout = scheduler.schedule(() -> { + synchronized (HttpConnection.this) { + if (state != State.RECEIVING_HEADER && state != State.RECEIVING_BODY) { + return; + } + + responseTimeout = null; + changeState(State.RESPONSE_TIMEOUT); + close(); + } + }, configuration.getResponseTimeout(), TimeUnit.MILLISECONDS); + } + + private void cancelResponseTimeout() { + if (responseTimeout != null) { + responseTimeout.cancel(true); + responseTimeout = null; + } + } + + private void scheduleConnectTimeout() { + if (configuration.getConnectTimeout() == 0) { + return; + } + + connectTimeout = scheduler.schedule(() -> { + synchronized (HttpConnection.this) { + if (state != State.CONNECTING) { + return; + } + + connectTimeout = null; + changeState(State.CONNECT_TIMEOUT); + close(); + } + }, configuration.getConnectTimeout(), TimeUnit.MILLISECONDS); + } + + private void cancelConnectTimeout() { + if (connectTimeout != null) { + connectTimeout.cancel(true); + connectTimeout = null; + } + } + + private void scheduleIdleTimeout() { + if (configuration.getConnectionIdleTimeout() == 0) { + return; + } + + idleTimeout = scheduler.schedule(() -> { + synchronized (HttpConnection.this) { + if (state != State.IDLE) { + return; + } + idleTimeout = null; + changeState(State.IDLE_TIMEOUT); + close(); + } + }, configuration.getConnectionIdleTimeout(), TimeUnit.MILLISECONDS); + } + + private void cancelIdleTimeout() { + if (idleTimeout != null) { + idleTimeout.cancel(true); + idleTimeout = null; + } + } + + private void cancelAllTimeouts() { + cancelConnectTimeout(); + cancelIdleTimeout(); + cancelResponseTimeout(); + } + + private synchronized void handleError(Throwable t) { + cancelAllTimeouts(); + error = t; + changeState(State.ERROR); + close(); + } + + private void changeStateToIdle() { + scheduleIdleTimeout(); + changeState(State.IDLE); + } + + Throwable getError() { + return error; + } + + HttpResponse getHttResponse() { + return httResponse; + } + + private synchronized void handleResponseRead() { + cancelResponseTimeout(); + changeState(State.RECEIVED); + if (!persistentConnection) { + changeState(State.CLOSED); + return; + } + changeStateToIdle(); + } + + private class ConnectionFilter extends Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> { + + ConnectionFilter(Filter<HttpRequest, HttpResponse, ?, ?> downstreamFilter) { + super(downstreamFilter); + } + + @Override + boolean processRead(HttpResponse response) { + synchronized (HttpConnection.this) { + if (state != State.RECEIVING_HEADER && state != State.SENDING_REQUEST) { + return false; + } + + if (state == State.SENDING_REQUEST) { + // great we received response header so fast that we did not even switch into "receiving header" state, + // do it now to complete the formal lifecycle + // this happens when write completion listener is overtaken by "read event" + changeState(State.RECEIVING_HEADER); + } + + httResponse = response; + + try { + processResponseHeaders(response); + } catch (IOException e) { + handleError(e); + return false; + } + } + + if (response.getHasContent()) { + AsynchronousBodyInputStream bodyStream = httResponse.getBodyStream(); + changeState(State.RECEIVING_BODY); + bodyStream.setStateChangeLister(new AsynchronousBodyInputStream.StateChangeLister() { + @Override + public void onError(Throwable t) { + handleError(t); + } + + @Override + public void onAllDataRead() { + handleResponseRead(); + } + }); + + } else { + handleResponseRead(); + } + return false; + } + + @Override + void processConnect() { + synchronized (HttpConnection.this) { + if (state != State.CONNECTING) { + return; + } + + downstreamFilter.startSsl(); + } + } + + @Override + void processSslHandshakeCompleted() { + synchronized (HttpConnection.this) { + if (state != State.CONNECTING) { + return; + } + + cancelConnectTimeout(); + changeStateToIdle(); + } + } + + @Override + void processConnectionClosed() { + cancelAllTimeouts(); + changeState(State.CLOSED_BY_SERVER); + HttpConnection.this.close(); + } + + @Override + void processError(Throwable t) { + handleError(t); + } + + @Override + void write(HttpRequest data, CompletionHandler<HttpRequest> completionHandler) { + downstreamFilter.write(data, completionHandler); + } + } + + enum State { + CREATED, + CONNECTING, + CONNECT_TIMEOUT, + IDLE, + SENDING_REQUEST, + RECEIVING_HEADER, + RECEIVING_BODY, + RECEIVED, + RESPONSE_TIMEOUT, + CLOSED_BY_SERVER, + CLOSED, + ERROR, + IDLE_TIMEOUT + } + + interface StateChangeListener { + + void onStateChanged(HttpConnection connection, State oldState, State newState); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java new file mode 100644 index 0000000..96842c4 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.CookieManager; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpConnectionPool { + + // TODO better solution, containers won't like this + private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); + + private final ConnectorConfiguration connectorConfiguration; + private final CookieManager cookieManager; + private final Map<DestinationConnectionPool.DestinationKey, DestinationConnectionPool> destinationPools = new + ConcurrentHashMap<>(); + + HttpConnectionPool(ConnectorConfiguration connectorConfiguration, CookieManager cookieManager) { + this.connectorConfiguration = connectorConfiguration; + this.cookieManager = cookieManager; + } + + void send(HttpRequest httpRequest, CompletionHandler<HttpResponse> completionHandler) { + final DestinationConnectionPool.DestinationKey destinationKey = new DestinationConnectionPool.DestinationKey( + httpRequest.getUri()); + DestinationConnectionPool destinationConnectionPool = destinationPools.get(destinationKey); + + if (destinationConnectionPool == null) { + synchronized (this) { + // check again while holding the lock + destinationConnectionPool = destinationPools.get(destinationKey); + + if (destinationConnectionPool == null) { + final DestinationConnectionPool pool = new DestinationConnectionPool(connectorConfiguration, cookieManager, + scheduler); + pool.setConnectionCloseListener(() -> { + /* There is a potential race when there is a request just about to be submitted to the pool + we are just removing. Such request will be executed on the removed pool without any problems. + The only issue is that this listener will be called for the second time in such a case, so we + have to make sure we don't remove a new pool that might have been created in the meantime. */ + destinationPools.remove(destinationKey, pool); + }); + + destinationConnectionPool = pool; + destinationPools.put(destinationKey, destinationConnectionPool); + } + } + } + + destinationConnectionPool.send(httpRequest, completionHandler); + } + + synchronized void close() { + destinationPools.values().forEach(DestinationConnectionPool::close); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpFilter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpFilter.java new file mode 100644 index 0000000..3f3dda8 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpFilter.java
@@ -0,0 +1,129 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.URI; +import java.nio.ByteBuffer; + +import javax.ws.rs.core.HttpHeaders; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpFilter extends Filter<HttpRequest, HttpResponse, ByteBuffer, ByteBuffer> { + + private final HttpParser httpParser; + + /** + * Constructor. + * + * @param downstreamFilter downstream filter. Accessible directly as {@link #downstreamFilter} protected field. + */ + HttpFilter(Filter<ByteBuffer, ByteBuffer, ?, ?> downstreamFilter, int maxHeaderSize, int maxBufferSize) { + super(downstreamFilter); + this.httpParser = new HttpParser(maxHeaderSize, maxBufferSize); + } + + @Override + void write(final HttpRequest httpRequest, final CompletionHandler<HttpRequest> completionHandler) { + addTransportHeaders(httpRequest); + + ByteBuffer header = HttpRequestEncoder.encodeHeader(httpRequest); + prepareForReply(httpRequest, completionHandler); + downstreamFilter.write(header, new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable throwable) { + completionHandler.failed(throwable); + } + + @Override + public void completed(ByteBuffer result) { + writeBody(httpRequest, completionHandler); + } + }); + } + + private void writeBody(final HttpRequest httpRequest, final CompletionHandler<HttpRequest> completionHandler) { + switch (httpRequest.getBodyMode()) { + + case CHUNKED: { + ChunkedBodyOutputStream bodyStream = (ChunkedBodyOutputStream) httpRequest.getBodyStream(); + bodyStream.open(downstreamFilter); + break; + } + + case BUFFERED: { + ByteBuffer body = httpRequest.getBufferedBody(); + downstreamFilter.write(body, new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable throwable) { + completionHandler.failed(throwable); + } + }); + + break; + } + } + } + + private void prepareForReply(HttpRequest httpRequest, CompletionHandler<HttpRequest> completionHandler) { + completionHandler.completed(httpRequest); + + boolean expectResponseBody = true; + + if (Constants.HEAD.equals(httpRequest.getMethod()) || Constants.CONNECT.equals(httpRequest.getMethod())) { + expectResponseBody = false; + } + + httpParser.reset(expectResponseBody); + } + + @Override + boolean processRead(ByteBuffer data) { + boolean headerParsed = httpParser.isHeaderParsed(); + try { + httpParser.parse(data); + } catch (ParseException e) { + onError(e); + } + + if (!headerParsed && httpParser.isHeaderParsed()) { + HttpResponse httpResponse = httpParser.getHttpResponse(); + upstreamFilter.onRead(httpResponse); + } + + return false; + } + + private void addTransportHeaders(HttpRequest httpRequest) { + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.BUFFERED) { + httpRequest.addHeaderIfNotPresent(Constants.CONTENT_LENGTH, Integer.toString(httpRequest.getBodySize())); + } + + URI uri = httpRequest.getUri(); + int port = Utils.getPort(uri); + httpRequest.addHeaderIfNotPresent(Constants.HOST, uri.getHost() + ":" + port); + + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.CHUNKED) { + httpRequest.addHeaderIfNotPresent(Constants.TRANSFER_ENCODING_HEADER, Constants.TRANSFER_ENCODING_CHUNKED); + } + + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.NONE) { + httpRequest.addHeaderIfNotPresent(HttpHeaders.CONTENT_LENGTH, Integer.toString(0)); + } + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpParser.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpParser.java new file mode 100644 index 0000000..3170e06 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpParser.java
@@ -0,0 +1,619 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.List; + +import javax.ws.rs.core.HttpHeaders; + +/** + * @author Alexey Stashok + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpParser { + + private static final String ENCODING = "ISO-8859-1"; + + private static final int BUFFER_STEP_SIZE = 256; + // this is package private because of the test + static final int INIT_BUFFER_SIZE = 1024; + + private final HttpParserUtils.HeaderParsingState headerParsingState; + private final int bufferMaxSize; + private final int maxHeaderSize; + + private volatile ByteBuffer buffer = ByteBuffer.allocate(INIT_BUFFER_SIZE); + private volatile boolean headerParsed; + private volatile boolean expectContent; + private volatile String protocolVersion; + private volatile int code; + + private volatile HttpResponse httpResponse; + private volatile TransferEncodingParser transferEncodingParser; + private volatile boolean complete; + + HttpParser(int maxHeaderSize, int bufferMaxSize) { + headerParsingState = new HttpParserUtils.HeaderParsingState(maxHeaderSize); + this.bufferMaxSize = bufferMaxSize; + this.maxHeaderSize = maxHeaderSize; + } + + void reset(boolean expectContent) { + this.expectContent = expectContent; + headerParsed = false; + buffer.clear(); + buffer.flip(); + complete = false; + headerParsingState.recycle(); + } + + boolean isHeaderParsed() { + return headerParsed; + } + + boolean isComplete() { + return complete; + } + + HttpResponse getHttpResponse() { + return httpResponse; + } + + void parse(ByteBuffer input) throws ParseException { + if (buffer.remaining() > 0) { + input = Utils.appendBuffers(buffer, input, bufferMaxSize, BUFFER_STEP_SIZE); + } + + if (!headerParsed && !parseHeader(input)) { + saveRemaining(input); + return; + } + + httpResponse.setHasContent(expectContent); + if (expectContent) { + if (transferEncodingParser.parse(input)) { + complete = true; + } else { + saveRemaining(input); + } + } else { + // We don't expect content + complete = true; + } + + if (complete && input.hasRemaining()) { + throw new ParseException(LocalizationMessages.UNEXPECTED_DATA_IN_BUFFER()); + } + + if (complete) { + httpResponse.getBodyStream().notifyAllDataRead(); + } + } + + private void saveRemaining(ByteBuffer input) { + + // some of the fields use 0 nad -1 as a special state -> if the field is <= 0, just let it be + headerParsingState.start = + headerParsingState.start > 0 ? headerParsingState.start - input.position() : headerParsingState.start; + headerParsingState.offset = + headerParsingState.offset > 0 ? headerParsingState.offset - input.position() : headerParsingState.offset; + headerParsingState.packetLimit = headerParsingState.packetLimit > 0 ? headerParsingState.packetLimit - input.position() + : headerParsingState.packetLimit; + headerParsingState.checkpoint = headerParsingState.checkpoint > 0 ? headerParsingState.checkpoint - input.position() + : headerParsingState.checkpoint; + headerParsingState.checkpoint2 = headerParsingState.checkpoint2 > 0 ? headerParsingState.checkpoint2 - input.position() + : headerParsingState.checkpoint2; + + if (input.hasRemaining()) { + if (input != buffer) { + buffer.clear(); + buffer.flip(); + buffer = Utils.appendBuffers(buffer, input, bufferMaxSize, BUFFER_STEP_SIZE); + } else { + buffer.compact(); + buffer.flip(); + } + } + } + + // Taken with small modifications from Grizzly HttpCodecFilter.parseHeaderFromBuffer + // (change: operations in phase 2 are translated to fit this parser) + private boolean parseHeader(ByteBuffer input) throws ParseException { + + while (true) { + switch (headerParsingState.state) { + case 0: { // parsing initial line + if (!decodeInitialLineFromBuffer(input)) { + headerParsingState.checkOverflow(LocalizationMessages.HTTP_INITIAL_LINE_OVERFLOW()); + return false; + } + + headerParsingState.state++; + break; + } + + case 1: { // parsing headers + if (!parseHeadersFromBuffer(input, false)) { + headerParsingState.checkOverflow(LocalizationMessages.HTTP_PACKET_HEADER_OVERFLOW()); + return false; + } + + headerParsingState.state++; + break; + } + + case 2: { // Headers are ready + input.position(headerParsingState.offset); + // if headers get parsed - set the flag + headerParsed = true; + decideTransferEncoding(); + + // recycle header parsing state + headerParsingState.recycle(); + return true; + } + + default: + throw new IllegalStateException(); + } + } + } + + // Taken unmodified from Grizzly HttpClientFilter.decodeInitialLineFromBuffer + private boolean decodeInitialLineFromBuffer(final ByteBuffer input) throws ParseException { + + final int packetLimit = headerParsingState.packetLimit; + + //noinspection LoopStatementThatDoesntLoop + while (true) { + int subState = headerParsingState.subState; + + switch (subState) { + case 0: { // HTTP protocol + final int spaceIdx = + findSpace(input, headerParsingState.offset, packetLimit); + if (spaceIdx == -1) { + headerParsingState.offset = input.limit(); + return false; + } + + protocolVersion = parseString(input, headerParsingState.start, spaceIdx); + + headerParsingState.start = -1; + headerParsingState.offset = spaceIdx; + + headerParsingState.subState++; + break; + } + + case 1: { // skip spaces after the HTTP protocol + final int nonSpaceIdx = + HttpParserUtils.skipSpaces(input, headerParsingState.offset, packetLimit); + if (nonSpaceIdx == -1) { + headerParsingState.offset = input.limit(); + return false; + } + + headerParsingState.start = nonSpaceIdx; + headerParsingState.offset = nonSpaceIdx + 1; + headerParsingState.subState++; + break; + } + + case 2: { // parse the status code + if (headerParsingState.offset + 3 > input.limit()) { + return false; + } + + code = parseInt(input, headerParsingState.start, headerParsingState.start + 3); + + headerParsingState.start = -1; + headerParsingState.offset += 3; + headerParsingState.subState++; + break; + } + + case 3: { // skip spaces after the status code + final int nonSpaceIdx = + HttpParserUtils.skipSpaces(input, headerParsingState.offset, packetLimit); + if (nonSpaceIdx == -1) { + headerParsingState.offset = input.limit(); + return false; + } + + headerParsingState.start = nonSpaceIdx; + headerParsingState.offset = nonSpaceIdx; + headerParsingState.subState++; + break; + } + + case 4: { // HTTP response reason-phrase + if (!findEOL(input)) { + headerParsingState.offset = input.limit(); + return false; + } + + String reasonPhrase = parseString(input, headerParsingState.start, headerParsingState.checkpoint); + + headerParsingState.subState = 0; + headerParsingState.start = -1; + headerParsingState.checkpoint = -1; + httpResponse = new HttpResponse(protocolVersion, code, reasonPhrase); + + if (httpResponse.getStatusCode() == 100) { + // reset the parsing state in preparation to parse + // another initial line which represents the final + // response from the server after it has sent a + // 100-Continue. + headerParsingState.offset += 2; + headerParsingState.start = 0; + input.position(headerParsingState.offset); + input.compact(); + headerParsingState.offset = 0; + return false; + } + + return true; + } + + default: + throw new IllegalStateException(); + } + } + } + + // Taken unmodified from Grizzly from HttpCodecFilter.parseHeadersFromBuffer + boolean parseHeadersFromBuffer(final ByteBuffer input, boolean parsingTrailerHeaders) throws ParseException { + do { + if (headerParsingState.subState == 0) { + final int eol = checkEOL(input); + if (eol == 0) { // EOL + return true; + } else if (eol == -2) { // not enough data + return false; + } + } + + if (!parseHeaderFromBuffer(input, parsingTrailerHeaders)) { + return false; + } + + } while (true); + } + + // Taken unmodified from Grizzly HttpCodecFilter.parseHeaderFromBuffer + private boolean parseHeaderFromBuffer(final ByteBuffer input, boolean parsingTrailerHeaders) throws ParseException { + + while (true) { + final int subState = headerParsingState.subState; + + switch (subState) { + case 0: { // start to parse the header + headerParsingState.start = headerParsingState.offset; + headerParsingState.subState++; + break; + } + case 1: { // parse header name + if (!parseHeaderName(input)) { + return false; + } + + headerParsingState.subState++; + headerParsingState.start = -1; + break; + } + + case 2: { // skip value preceding spaces + final int nonSpaceIdx = HttpParserUtils + .skipSpaces(input, headerParsingState.offset, headerParsingState.packetLimit); + if (nonSpaceIdx == -1) { + headerParsingState.offset = input.limit(); + return false; + } + + headerParsingState.subState++; + headerParsingState.offset = nonSpaceIdx; + + if (headerParsingState.start == -1) { + // Starting to parse header (will be called only for the first line of the multi line header) + headerParsingState.start = nonSpaceIdx; + headerParsingState.checkpoint = nonSpaceIdx; + headerParsingState.checkpoint2 = nonSpaceIdx; + } + break; + } + + case 3: { // parse header value + final int result = parseHeaderValue(input, parsingTrailerHeaders); + if (result == -1) { + return false; + } else if (result == -2) { + // Multiline header detected. Skip preceding spaces + headerParsingState.subState = 2; + break; + } + + headerParsingState.subState = 0; + headerParsingState.start = -1; + + return true; + } + + default: + throw new IllegalStateException(); + } + } + } + + // Taken with small modifications from Grizzly HttpCodecFilter.parseHeaderName + // (change: Grizzly also initializes value store) + private boolean parseHeaderName(final ByteBuffer input) throws ParseException { + final int limit = Math.min(input.limit(), headerParsingState.packetLimit); + final int start = headerParsingState.start; + int offset = headerParsingState.offset; + + while (offset < limit) { + byte b = input.get(offset); + if (b == HttpParserUtils.COLON) { + + headerParsingState.headerName = parseString(input, start, offset); + headerParsingState.offset = offset + 1; + + return true; + } else if ((b >= HttpParserUtils.A) && (b <= HttpParserUtils.Z)) { + b -= HttpParserUtils.LC_OFFSET; + input.put(offset, b); + } + + offset++; + } + + headerParsingState.offset = offset; + return false; + } + + // Taken with small modifications from Grizzly HttpCodecFilter.parseHeaderValue + // (change: Grizzly saves teh value as a buffer, we split it and add to response) + private int parseHeaderValue(ByteBuffer input, boolean parsingTrailerHeaders) throws ParseException { + + final int limit = Math.min(input.limit(), headerParsingState.packetLimit); + + int offset = headerParsingState.offset; + + final boolean hasShift = (offset != headerParsingState.checkpoint); + + while (offset < limit) { + final byte b = input.get(offset); + /* This if is not in Grizzly, it is used for parsing comma separated values. + Grizzly separates the header in Header class. */ + if (b == HttpParserUtils.COMMA && !isInseparableHeader()) { + headerParsingState.offset = offset + 1; + String value = parseString(input, + headerParsingState.start, headerParsingState.checkpoint2); + httpResponse.addHeader(headerParsingState.headerName, value); + headerParsingState.start = headerParsingState.checkpoint2; + return -2; + } + + if (b == HttpParserUtils.CR) { + // do nothing + } else if (b == HttpParserUtils.LF) { + // Check if it's not multi line header + if (offset + 1 < limit) { + final byte b2 = input.get(offset + 1); + if (b2 == HttpParserUtils.SP || b2 == HttpParserUtils.HT) { + input.put(headerParsingState.checkpoint++, b2); + headerParsingState.offset = offset + 2; + return -2; + } else { + headerParsingState.offset = offset + 1; + String value = parseString(input, + headerParsingState.start, headerParsingState.checkpoint2); + if (parsingTrailerHeaders) { + httpResponse.addTrailerHeader(headerParsingState.headerName, value); + } else { + httpResponse.addHeader(headerParsingState.headerName, value); + } + return 0; + } + } + + headerParsingState.offset = offset; + return -1; + } else if (b == HttpParserUtils.SP) { + if (hasShift) { + input.put(headerParsingState.checkpoint++, b); + } else { + headerParsingState.checkpoint++; + } + } else { + if (hasShift) { + input.put(headerParsingState.checkpoint++, b); + } else { + headerParsingState.checkpoint++; + } + headerParsingState.checkpoint2 = headerParsingState.checkpoint; + } + + offset++; + } + + headerParsingState.offset = offset; + return -1; + } + + private boolean isInseparableHeader() { + /* Authenticate headers contain comma separated list of properties, which would be normally treated as separate header + values */ + return Constants.WWW_AUTHENTICATE.equalsIgnoreCase(headerParsingState.headerName) + || Constants.PROXY_AUTHENTICATE.equalsIgnoreCase(headerParsingState.headerName); + + } + + private void decideTransferEncoding() throws ParseException { + + int statusCode = httpResponse.getStatusCode(); + if (statusCode == 204 || statusCode == 205 || statusCode == 304) { + expectContent = false; + } + + if (httpResponse.getHeaders().size() == 0) { + expectContent = false; + } + + List<String> transferEncodings = httpResponse.getHeader(Constants.TRANSFER_ENCODING_HEADER); + + if (transferEncodings != null) { + String transferEncoding = transferEncodings.get(0); + if (Constants.TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(transferEncoding)) { + transferEncodingParser = TransferEncodingParser + .createChunkParser(httpResponse.getBodyStream(), this, maxHeaderSize); + } + + return; + } + + List<String> contentLengths = httpResponse.getHeader(HttpHeaders.CONTENT_LENGTH); + + if (contentLengths != null) { + try { + int bodyLength = Integer.parseInt(contentLengths.get(0)); + if (bodyLength == 0) { + expectContent = false; + return; + } + + if (bodyLength <= 0) { + throw new ParseException(LocalizationMessages.HTTP_NEGATIVE_CONTENT_LENGTH()); + } + + transferEncodingParser = TransferEncodingParser + .createFixedLengthParser(httpResponse.getBodyStream(), bodyLength); + + } catch (NumberFormatException e) { + throw new ParseException(LocalizationMessages.HTTP_INVALID_CONTENT_LENGTH()); + } + + return; + } + + // TODO what now? Expect no content or fail loudly? + } + + // Taken unmodified from Grizzly HttpCodecUtils.findSpace + private int findSpace(final ByteBuffer input, int offset, final int packetLimit) { + final int limit = Math.min(input.limit(), packetLimit); + while (offset < limit) { + final byte b = input.get(offset); + if (HttpParserUtils.isSpaceOrTab(b)) { + return offset; + } + + offset++; + } + + return -1; + } + + // Taken unmodified from Grizzly HttpCodecUtils.findEOL + private boolean findEOL(final ByteBuffer input) { + int offset = headerParsingState.offset; + final int limit = Math.min(input.limit(), headerParsingState.packetLimit); + + while (offset < limit) { + final byte b = input.get(offset); + if (b == HttpParserUtils.CR) { + headerParsingState.checkpoint = offset; + } else if (b == HttpParserUtils.LF) { + if (headerParsingState.checkpoint == -1) { + headerParsingState.checkpoint = offset; + } + + headerParsingState.offset = offset + 1; + return true; + } + + offset++; + } + + headerParsingState.offset = offset; + + return false; + } + + // Taken unmodified from Grizzly HttpCodecUtils.checkEOL + private int checkEOL(final ByteBuffer input) { + final int offset = headerParsingState.offset; + final int avail = input.limit() - offset; + + final byte b1; + final byte b2; + + if (avail >= 2) { // if more than 2 bytes available + final short s = input.getShort(offset); + b1 = (byte) (s >>> 8); + b2 = (byte) (s & 0xFF); + } else if (avail == 1) { // if one byte available + b1 = input.get(offset); + b2 = -1; + } else { + return -2; + } + + return checkCRLF(b1, b2); + } + + // Taken unmodified from Grizzly HttpCodecUtils.checkCRLF + private int checkCRLF(byte b1, byte b2) { + if (b1 == HttpParserUtils.CR) { + if (b2 == HttpParserUtils.LF) { + headerParsingState.offset += 2; + return 0; + } else if (b2 == -1) { + return -2; + } + } else if (b1 == HttpParserUtils.LF) { + headerParsingState.offset++; + return 0; + } + + return -1; + } + + HttpParserUtils.HeaderParsingState getHeaderParsingState() { + return headerParsingState; + } + + private String parseString(ByteBuffer input, int startIdx, int endIdx) throws ParseException { + byte[] bytes = new byte[endIdx - startIdx]; + input.position(startIdx); + input.get(bytes, 0, endIdx - startIdx); + try { + return new String(bytes, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new ParseException("Unsupported encoding: " + ENCODING, e); + } + + } + + private int parseInt(ByteBuffer input, int startIdx, int endIdx) throws ParseException { + String value = parseString(input, startIdx, endIdx); + return Integer.valueOf(value); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpParserUtils.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpParserUtils.java new file mode 100644 index 0000000..e07177d --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpParserUtils.java
@@ -0,0 +1,115 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.nio.ByteBuffer; + +/** + * @author Alexey Stashok + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpParserUtils { + + static final byte CR = (byte) '\r'; + static final byte LF = (byte) '\n'; + static final byte SP = (byte) ' '; + static final byte HT = (byte) '\t'; + static final byte COMMA = (byte) ','; + static final byte COLON = (byte) ':'; + static final byte SEMI_COLON = (byte) ';'; + static final byte A = (byte) 'A'; + static final byte Z = (byte) 'Z'; + static final byte a = (byte) 'a'; + static final byte LC_OFFSET = A - a; + + static int skipSpaces(ByteBuffer input, int offset, int packetLimit) { + final int limit = Math.min(input.limit(), packetLimit); + while (offset < limit) { + final byte b = input.get(offset); + if (isNotSpaceAndTab(b)) { + return offset; + } + + offset++; + } + + return -1; + } + + static boolean isNotSpaceAndTab(byte b) { + return !isSpaceOrTab(b); + } + + static boolean isSpaceOrTab(byte b) { + return (b == HttpParserUtils.SP || b == HttpParserUtils.HT); + } + + static class HeaderParsingState { + + final int maxHeaderSize; + int packetLimit; + + int state; + int subState; + + int start; + int offset; + int checkpoint = -1; // extra parsing state field + int checkpoint2 = -1; // extra parsing state field + + String headerName; + + long parsingNumericValue; + + int contentLengthHeadersCount; // number of Content-Length headers in the HTTP header + boolean contentLengthsDiffer; + + HeaderParsingState(int maxHeaderSize) { + this.maxHeaderSize = maxHeaderSize; + } + + void recycle() { + state = 0; + subState = 0; + start = 0; + offset = 0; + checkpoint = -1; + checkpoint2 = -1; + parsingNumericValue = 0; + contentLengthHeadersCount = 0; + contentLengthsDiffer = false; + headerName = null; + packetLimit = maxHeaderSize; + } + + void checkOverflow(String errorDescriptionIfOverflow) throws ParseException { + if (offset < packetLimit) { + return; + } + + throw new ParseException(errorDescriptionIfOverflow); + } + } + + static class ContentParsingState { + + boolean isLastChunk; + int chunkContentStart = -1; + long chunkLength = -1; + long chunkRemainder = -1; + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpRequest.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpRequest.java new file mode 100644 index 0000000..4eea826 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpRequest.java
@@ -0,0 +1,113 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpRequest { + + private final String method; + private final URI uri; + private final Map<String, List<String>> headers = new HashMap<>(); + private final BodyMode bodyMode; + private final BodyOutputStream bodyStream; + + private HttpRequest(String method, URI uri, BodyMode bodyMode, BodyOutputStream bodyStream) { + this.method = method; + this.uri = uri; + this.bodyMode = bodyMode; + this.bodyStream = bodyStream; + } + + static HttpRequest createBodyless(String method, URI uri) { + HttpRequest httpRequest = new HttpRequest(method, uri, BodyMode.NONE, null); + return httpRequest; + } + + static HttpRequest createChunked(String method, URI uri, int chunkSize) { + ChunkedBodyOutputStream bodyStream = new ChunkedBodyOutputStream(chunkSize); + return new HttpRequest(method, uri, BodyMode.CHUNKED, bodyStream); + } + + static HttpRequest createBuffered(String method, URI uri) { + BufferedBodyOutputStream bodyOutputStream = new BufferedBodyOutputStream(); + return new HttpRequest(method, uri, BodyMode.BUFFERED, bodyOutputStream); + } + + String getMethod() { + return method; + } + + URI getUri() { + return uri; + } + + Map<String, List<String>> getHeaders() { + return headers; + } + + BodyMode getBodyMode() { + return bodyMode; + } + + BodyOutputStream getBodyStream() { + if (BodyMode.NONE == bodyMode) { + throw new IllegalStateException(LocalizationMessages.HTTP_REQUEST_NO_BODY()); + } + + return bodyStream; + } + + void addHeaderIfNotPresent(String name, String value) { + List<String> values = headers.get(name); + if (values == null) { + values = new ArrayList<>(1); + headers.put(name, values); + values.add(value); + } + } + + ByteBuffer getBufferedBody() { + if (BodyMode.BUFFERED != bodyMode) { + throw new IllegalStateException(LocalizationMessages.HTTP_REQUEST_NO_BUFFERED_BODY()); + } + + return ((BufferedBodyOutputStream) bodyStream).toBuffer(); + } + + int getBodySize() { + if (bodyMode == BodyMode.CHUNKED) { + throw new IllegalStateException(LocalizationMessages.HTTP_REQUEST_BODY_SIZE_NOT_AVAILABLE()); + } + + return ((BufferedBodyOutputStream) bodyStream).toBuffer().remaining(); + } + + enum BodyMode { + NONE, + CHUNKED, + BUFFERED + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpRequestEncoder.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpRequestEncoder.java new file mode 100644 index 0000000..ae29c13 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpRequestEncoder.java
@@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpRequestEncoder { + + private static final String ENCODING = "ISO-8859-1"; + private static final String LINE_SEPARATOR = "\r\n"; + private static final byte[] LINE_SEPARATOR_BYTES = LINE_SEPARATOR.getBytes(Charset.forName(ENCODING)); + private static final byte[] LAST_CHUNK = "0\r\n\r\n".getBytes(Charset.forName(ENCODING)); + private static final String HTTP_VERSION = "HTTP/1.1"; + + private static void appendUpgradeHeaders(StringBuilder request, Map<String, List<String>> headers) { + for (Map.Entry<String, List<String>> header : headers.entrySet()) { + StringBuilder value = new StringBuilder(); + for (String valuePart : header.getValue()) { + if (value.length() != 0) { + value.append(","); + } + value.append(valuePart); + } + appendHeader(request, header.getKey(), value.toString()); + } + + request.append(LINE_SEPARATOR); + } + + private static void appendHeader(StringBuilder request, String key, String value) { + request.append(key); + request.append(": "); + request.append(value); + request.append(LINE_SEPARATOR); + } + + private static void appendFirstLine(StringBuilder request, HttpRequest httpRequest) { + request.append(httpRequest.getMethod()); + request.append(" "); + if (httpRequest.getMethod().equals(Constants.CONNECT)) { + request.append(httpRequest.getUri().toString()); + } else { + URI uri = httpRequest.getUri(); + String path = uri.getRawPath(); + if (path == null || path.isEmpty()) { + path = "/"; + } + + if (uri.getRawQuery() != null) { + path += "?" + uri.getRawQuery(); + } + + request.append(path); + } + request.append(" "); + request.append(HTTP_VERSION); + request.append(LINE_SEPARATOR); + } + + static ByteBuffer encodeHeader(HttpRequest httpRequest) { + StringBuilder request = new StringBuilder(); + appendFirstLine(request, httpRequest); + appendUpgradeHeaders(request, httpRequest.getHeaders()); + String requestStr = request.toString(); + byte[] bytes = requestStr.getBytes(Charset.forName(ENCODING)); + return ByteBuffer.wrap(bytes); + } + + static ByteBuffer encodeChunk(ByteBuffer data) { + if (data.remaining() == 0) { + return ByteBuffer.wrap(LAST_CHUNK); + } + + byte[] startBytes = getChunkHeaderBytes(data.remaining()); + ByteBuffer chunkBuffer = ByteBuffer.allocate(startBytes.length + data.remaining() + 2); + chunkBuffer.put(startBytes); + chunkBuffer.put(data); + chunkBuffer.put(LINE_SEPARATOR_BYTES); + chunkBuffer.flip(); + + return chunkBuffer; + } + + private static byte[] getChunkHeaderBytes(int dataLength) { + String chunkStart = Integer.toHexString(dataLength) + LINE_SEPARATOR; + return chunkStart.getBytes(Charset.forName(ENCODING)); + } + + static int getChunkSize(int dataLength) { + if (dataLength == 0) { + return LAST_CHUNK.length; + } + + return getChunkHeaderBytes(dataLength).length + dataLength + 2; + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpResponse.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpResponse.java new file mode 100644 index 0000000..51aa7e1 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpResponse.java
@@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class HttpResponse { + + private final String protocolVersion; + private final int statusCode; + private final String reasonPhrase; + private final Map<String, List<String>> headers = new HashMap<>(); + private final Map<String, List<String>> trailerHeaders = new HashMap<>(0); + private final AsynchronousBodyInputStream bodyStream; + private volatile boolean hasContent = true; + + HttpResponse(String protocolVersion, int statusCode, String reasonPhrase) { + this.protocolVersion = protocolVersion; + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + bodyStream = new AsynchronousBodyInputStream(); + } + + String getProtocolVersion() { + return protocolVersion; + } + + int getStatusCode() { + return statusCode; + } + + String getReasonPhrase() { + return reasonPhrase; + } + + void setHasContent(boolean hasContent) { + this.hasContent = hasContent; + } + + boolean getHasContent() { + return hasContent; + } + + Map<String, List<String>> getHeaders() { + return headers; + } + + List<String> getHeader(String name) { + for (String headerName : headers.keySet()) { + if (headerName.equalsIgnoreCase(name)) { + return headers.get(headerName); + } + } + + return null; + } + + void addHeader(String name, String value) { + List<String> values = getHeader(name); + if (values == null) { + values = new ArrayList<>(1); + headers.put(name, values); + } + + values.add(value); + } + + List<String> getTrailerHeader(String name) { + for (String headerName : trailerHeaders.keySet()) { + if (headerName.equalsIgnoreCase(name)) { + return trailerHeaders.get(headerName); + } + } + + return null; + } + + void addTrailerHeader(String name, String value) { + List<String> values = getTrailerHeader(name); + if (values == null) { + values = new ArrayList<>(1); + trailerHeaders.put(name, values); + } + + values.add(value); + } + + AsynchronousBodyInputStream getBodyStream() { + return bodyStream; + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/InterceptingOutputStream.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/InterceptingOutputStream.java new file mode 100644 index 0000000..61d1840 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/InterceptingOutputStream.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A stream that invokes {@link FirstCallListener} when any operation is invoked. + * {@link FirstCallListener} is invoked only once in the stream lifetime. + * + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class InterceptingOutputStream extends OutputStream { + + private final OutputStream wrappedStream; + private final FirstCallListener firstCallListener; + private volatile boolean listenerInvoked = false; + + InterceptingOutputStream(OutputStream wrappedStream, FirstCallListener firstCallListener) { + this.wrappedStream = wrappedStream; + this.firstCallListener = firstCallListener; + } + + @Override + public void write(byte[] b) throws IOException { + tryInvokingListener(); + wrappedStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + tryInvokingListener(); + wrappedStream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + tryInvokingListener(); + wrappedStream.flush(); + } + + @Override + public void close() throws IOException { + tryInvokingListener(); + wrappedStream.close(); + } + + @Override + public void write(int b) throws IOException { + tryInvokingListener(); + wrappedStream.write(b); + } + + private void tryInvokingListener() { + if (!listenerInvoked) { + listenerInvoked = true; + this.firstCallListener.onInvoked(); + } + } + + interface FirstCallListener { + + void onInvoked(); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/JdkConnector.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/JdkConnector.java new file mode 100644 index 0000000..fe723a5 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/JdkConnector.java
@@ -0,0 +1,248 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class JdkConnector implements Connector { + + private final HttpConnectionPool httpConnectionPool; + private final ConnectorConfiguration connectorConfiguration; + + public JdkConnector(Client client, Configuration config) { + connectorConfiguration = new ConnectorConfiguration(client, config); + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(connectorConfiguration.getCookiePolicy()); + httpConnectionPool = new HttpConnectionPool(connectorConfiguration, cookieManager); + } + + @Override + public ClientResponse apply(ClientRequest request) { + + Future<?> future = apply(request, new AsyncConnectorCallback() { + @Override + public void response(ClientResponse response) { + + } + + @Override + public void failure(Throwable failure) { + + } + }); + + try { + return (ClientResponse) future.get(); + } catch (Exception e) { + throw new ProcessingException(unwrapExecutionException(e)); + } + } + + private Throwable unwrapExecutionException(Throwable failure) { + return (failure != null && failure instanceof ExecutionException) ? failure.getCause() : failure; + } + + @Override + public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) { + final CompletableFuture<ClientResponse> responseFuture = new CompletableFuture<>(); + // just so we don't have to drag around both the future and callback + final AsyncConnectorCallback internalCallback = new AsyncConnectorCallback() { + @Override + public void response(ClientResponse response) { + callback.response(response); + responseFuture.complete(response); + } + + @Override + public void failure(Throwable failure) { + Throwable actualFailure = unwrapExecutionException(failure); + callback.failure(actualFailure); + responseFuture.completeExceptionally(actualFailure); + } + }; + + final HttpRequest httpRequest = createHttpRequest(request); + + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.BUFFERED) { + writeBufferedEntity(request, httpRequest, internalCallback); + } + + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.BUFFERED + || httpRequest.getBodyMode() == HttpRequest.BodyMode.NONE) { + send(request, httpRequest, internalCallback); + } + + if (httpRequest.getBodyMode() == HttpRequest.BodyMode.CHUNKED) { + + /* We wait with sending the request header until the body stream has been touched. + This is because of javax.ws.rs.ext.MessageBodyWriter, which says: + + "The message header map is mutable but any changes must be made before writing to the output stream since + the headers will be flushed prior to writing the message body" + + This means that the headers can change until body output stream is used. + */ + final InterceptingOutputStream bodyStream = new InterceptingOutputStream(httpRequest.getBodyStream(), + // send the prepared request when the stream is touched for the first time + () -> send(request, httpRequest, internalCallback)); + + request.setStreamProvider(contentLength -> bodyStream); + try { + request.writeEntity(); + } catch (IOException e) { + internalCallback.failure(e); + } + } + + return responseFuture; + } + + private void writeBufferedEntity(ClientRequest request, final HttpRequest httpRequest, AsyncConnectorCallback callback) { + request.setStreamProvider(contentLength -> httpRequest.getBodyStream()); + try { + request.writeEntity(); + } catch (IOException e) { + callback.failure(e); + } + } + + private void send(final ClientRequest request, final HttpRequest httpRequest, final AsyncConnectorCallback callback) { + translateHeaders(request, httpRequest); + final RedirectHandler redirectHandler = new RedirectHandler(httpConnectionPool, httpRequest, connectorConfiguration); + httpConnectionPool.send(httpRequest, new CompletionHandler<HttpResponse>() { + + @Override + public void failed(Throwable throwable) { + callback.failure(throwable); + } + + @Override + public void completed(HttpResponse result) { + redirectHandler.handleRedirects(result, new CompletionHandler<HttpResponse>() { + @Override + public void failed(Throwable throwable) { + Throwable actualFailure = unwrapExecutionException(throwable); + callback.failure(actualFailure); + } + + @Override + public void completed(HttpResponse result) { + ClientResponse response = translateResponse(request, result, redirectHandler.getLastRequestUri()); + callback.response(response); + } + }); + } + }); + } + + private HttpRequest createHttpRequest(ClientRequest request) { + Object entity = request.getEntity(); + + if (entity == null) { + return HttpRequest.createBodyless(request.getMethod(), request.getUri()); + } + + RequestEntityProcessing entityProcessing = request.resolveProperty( + ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + + HttpRequest httpRequest; + if (entityProcessing != null && entityProcessing == RequestEntityProcessing.CHUNKED) { + httpRequest = HttpRequest.createChunked(request.getMethod(), request.getUri(), connectorConfiguration.getChunkSize()); + } else { + httpRequest = HttpRequest.createBuffered(request.getMethod(), request.getUri()); + } + + return httpRequest; + } + + private Map<String, List<String>> translateHeaders(ClientRequest clientRequest, HttpRequest httpRequest) { + Map<String, List<String>> headers = httpRequest.getHeaders(); + for (Map.Entry<String, List<String>> header : clientRequest.getStringHeaders().entrySet()) { + List<String> values = new ArrayList<>(header.getValue()); + headers.put(header.getKey(), values); + } + + return headers; + } + + private ClientResponse translateResponse(final ClientRequest requestContext, + final HttpResponse httpResponse, + URI requestUri) { + + Response.StatusType statusType = new Response.StatusType() { + @Override + public int getStatusCode() { + return httpResponse.getStatusCode(); + } + + @Override + public Response.Status.Family getFamily() { + return Response.Status.Family.familyOf(httpResponse.getStatusCode()); + } + + @Override + public String getReasonPhrase() { + return httpResponse.getReasonPhrase(); + } + }; + + ClientResponse responseContext = new ClientResponse(statusType, requestContext, requestUri); + + Map<String, List<String>> headers = httpResponse.getHeaders(); + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + for (String value : entry.getValue()) { + responseContext.getHeaders().add(entry.getKey(), value); + } + } + + responseContext.setEntityStream(httpResponse.getBodyStream()); + return responseContext; + } + + @Override + public String getName() { + return "JDK connector"; + } + + @Override + public void close() { + httpConnectionPool.close(); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ParseException.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ParseException.java new file mode 100644 index 0000000..37dab60 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ParseException.java
@@ -0,0 +1,33 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class ParseException extends Exception { + + private static final long serialVersionUID = 689526483137789578L; + + ParseException(String message) { + super(message); + } + + ParseException(String message, Throwable cause) { + super(message, cause); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyAuthenticationException.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyAuthenticationException.java new file mode 100644 index 0000000..a9a85fb --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyAuthenticationException.java
@@ -0,0 +1,27 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class ProxyAuthenticationException extends Exception { + + ProxyAuthenticationException(String message) { + super(message); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyBasicAuthenticator.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyBasicAuthenticator.java new file mode 100644 index 0000000..f7d7777 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyBasicAuthenticator.java
@@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.nio.charset.Charset; + +import org.glassfish.jersey.internal.util.Base64; + +/** + * @author Ondrej Kosatka (ondrej.kosatka at oracle.com) + */ +class ProxyBasicAuthenticator { + + /** + * Encoding used for authentication calculations. + */ + private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1"); + + static String generateAuthorizationHeader(String userName, String password) throws ProxyAuthenticationException { + if (userName == null) { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_USER_NAME_MISSING()); + } + + if (password == null) { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_PASSWORD_MISSING()); + } + + byte[] prefix = (userName + ":").getBytes(CHARACTER_SET); + byte[] passwordBytes = password.getBytes(CHARACTER_SET); + byte[] usernamePassword = new byte[prefix.length + passwordBytes.length]; + + System.arraycopy(prefix, 0, usernamePassword, 0, prefix.length); + System.arraycopy(passwordBytes, 0, usernamePassword, prefix.length, passwordBytes.length); + + return "Basic " + Base64.encodeAsString(usernamePassword); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyDigestAuthenticator.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyDigestAuthenticator.java new file mode 100644 index 0000000..285eeb5 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyDigestAuthenticator.java
@@ -0,0 +1,396 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Generates a value of {@code Authorization} header of HTTP request for Digest Http Authentication scheme (RFC 2617). + * + * @author raphael.jolivet@gmail.com + * @author Stefan Katerkamp (stefan@katerkamp.de) + * @author Miroslav Fuksa (miroslav.fuksa at oracle.com) + * @author Ondrej Kosatka (ondrej.kosatka at oracle.com) + */ +class ProxyDigestAuthenticator { + + /** + * Encoding used for authentication calculations. + */ + private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1"); + + private static final Logger logger = Logger.getLogger(ProxyDigestAuthenticator.class.getName()); + + private static final char[] HEX_ARRAY = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final Pattern KEY_VALUE_PAIR_PATTERN = + Pattern.compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|(\\w+))\\s*,?\\s*"); + private static final int CLIENT_NONCE_BYTE_COUNT = 4; + + private SecureRandom randomGenerator; + + ProxyDigestAuthenticator() { + try { + randomGenerator = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + logger.config("No such algorithm to generate authorization digest http header." + e); + } + } + + String generateAuthorizationHeader(URI uri, String method, String authenticateHeader, String userName, String password) + throws ProxyAuthenticationException { + if (userName == null) { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_USER_NAME_MISSING()); + } + + if (password == null) { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_PASSWORD_MISSING()); + } + DigestScheme digestScheme; + try { + digestScheme = parseAuthHeaders(authenticateHeader); + } catch (IOException e) { + throw new ProxyAuthenticationException(e.getMessage()); + } + if (digestScheme == null) { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_FAIL_AUTH_HEADER()); + } + + return createNextAuthToken(digestScheme, uri.toString(), method, userName, password); + } + + /** + * Parse digest header. + * + * @param authHeader value of {@code WWW-Authenticate} header + * @return DigestScheme or {@code null} if no digest header exists. + */ + private DigestScheme parseAuthHeaders(final String authHeader) throws IOException { + + if (authHeader == null) { + return null; + } + + String[] parts = authHeader.trim().split("\\s+", 2); + + if (parts.length != 2) { + return null; + } + if (!parts[0].toLowerCase().equals("digest")) { + return null; + } + + String realm = null; + String nonce = null; + String opaque = null; + QOP qop = QOP.UNSPECIFIED; + Algorithm algorithm = Algorithm.UNSPECIFIED; + boolean stale = false; + + Matcher match = KEY_VALUE_PAIR_PATTERN.matcher(parts[1]); + while (match.find()) { + // expect 4 groups (key)=("(val)" | (val)) + int nbGroups = match.groupCount(); + if (nbGroups != 4) { + continue; + } + String key = match.group(1); + String valNoQuotes = match.group(3); + String valQuotes = match.group(4); + String val = (valNoQuotes == null) ? valQuotes : valNoQuotes; + if (key.equals("qop")) { + qop = QOP.parse(val); + } else if (key.equals("realm")) { + realm = val; + } else if (key.equals("nonce")) { + nonce = val; + } else if (key.equals("opaque")) { + opaque = val; + } else if (key.equals("stale")) { + stale = Boolean.parseBoolean(val); + } else if (key.equals("algorithm")) { + algorithm = Algorithm.parse(val); + } + } + return new DigestScheme(realm, nonce, opaque, qop, algorithm, stale); + } + + /** + * Creates digest string including counter. + * + * @param ds DigestScheme instance + * @param uri client request uri + * @return digest authentication token string + * @throws ProxyAuthenticationException if MD5 hash fails + */ + private String createNextAuthToken(final DigestScheme ds, String uri, String method, String userName, String password) throws + ProxyAuthenticationException { + StringBuilder sb = new StringBuilder(100); + sb.append("Digest "); + append(sb, "username", userName); + append(sb, "realm", ds.getRealm()); + append(sb, "nonce", ds.getNonce()); + append(sb, "opaque", ds.getOpaque()); + append(sb, "algorithm", ds.getAlgorithm().toString(), false); + append(sb, "qop", ds.getQop().toString(), false); + + append(sb, "uri", uri); + + String ha1; + if (ds.getAlgorithm().equals(Algorithm.MD5_SESS)) { + ha1 = md5(md5(userName, ds.getRealm(), password)); + } else { + ha1 = md5(userName, ds.getRealm(), password); + } + + String ha2 = md5(method, uri); + + String response; + if (ds.getQop().equals(QOP.UNSPECIFIED)) { + response = md5(ha1, ds.getNonce(), ha2); + } else { + String cnonce = randomBytes(CLIENT_NONCE_BYTE_COUNT); // client nonce + append(sb, "cnonce", cnonce); + String nc = String.format("%08x", ds.incrementCounter()); // counter + append(sb, "nc", nc, false); + response = md5(ha1, ds.getNonce(), nc, cnonce, ds.getQop().toString(), ha2); + } + append(sb, "response", response); + + return sb.toString(); + } + + /** + * Append comma separated key=value token + * + * @param sb string builder instance + * @param key key string + * @param value value string + * @param useQuote true if value needs to be enclosed in quotes + */ + private static void append(StringBuilder sb, String key, String value, boolean useQuote) { + + if (value == null) { + return; + } + if (sb.length() > 0) { + if (sb.charAt(sb.length() - 1) != ' ') { + sb.append(", "); + } + } + sb.append(key); + sb.append('='); + if (useQuote) { + sb.append('"'); + } + sb.append(value); + if (useQuote) { + sb.append('"'); + } + } + + /** + * Append comma separated key=value token. The value gets enclosed in quotes. + * + * @param sb string builder instance + * @param key key string + * @param value value string + */ + private static void append(StringBuilder sb, String key, String value) { + append(sb, key, value, true); + } + + /** + * Convert bytes array to hex string. + * + * @param bytes array of bytes + * @return hex string + */ + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + int v; + for (int j = 0; j < bytes.length; j++) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Colon separated value MD5 hash. + * + * @param tokens one or more strings + * @return M5 hash string + * @throws ProxyAuthenticationException if MD5 algorithm cannot be instantiated + */ + private static String md5(String... tokens) throws ProxyAuthenticationException { + StringBuilder sb = new StringBuilder(100); + for (String token : tokens) { + if (sb.length() > 0) { + sb.append(':'); + } + sb.append(token); + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + throw new ProxyAuthenticationException(ex.getMessage()); + } + md.update(sb.toString().getBytes(CHARACTER_SET), 0, sb.length()); + byte[] md5hash = md.digest(); + return bytesToHex(md5hash); + } + + /** + * Generate a random sequence of bytes and return its hex representation + * + * @param nbBytes number of bytes to generate + * @return hex string + */ + private String randomBytes(int nbBytes) { + byte[] bytes = new byte[nbBytes]; + randomGenerator.nextBytes(bytes); + return bytesToHex(bytes); + } + + private enum QOP { + + UNSPECIFIED(null), + AUTH("auth"); + + private final String qop; + + QOP(String qop) { + this.qop = qop; + } + + @Override + public String toString() { + return qop; + } + + public static QOP parse(String val) { + if (val == null || val.isEmpty()) { + return QOP.UNSPECIFIED; + } + if (val.contains("auth")) { + return QOP.AUTH; + } + throw new UnsupportedOperationException(LocalizationMessages.PROXY_QOP_NO_SUPPORTED(val)); + } + } + + enum Algorithm { + + UNSPECIFIED(null), + MD5("MD5"), + MD5_SESS("MD5-sess"); + private final String md; + + Algorithm(String md) { + this.md = md; + } + + @Override + public String toString() { + return md; + } + + public static Algorithm parse(String val) { + if (val == null || val.isEmpty()) { + return Algorithm.UNSPECIFIED; + } + val = val.trim(); + if (val.contains(MD5_SESS.md) || val.contains(MD5_SESS.md.toLowerCase())) { + return MD5_SESS; + } + return MD5; + } + } + + /** + * Digest scheme POJO + */ + final class DigestScheme { + + private final String realm; + private final String nonce; + private final String opaque; + private final Algorithm algorithm; + private final QOP qop; + private final boolean stale; + private volatile int nc; + + DigestScheme(String realm, + String nonce, + String opaque, + QOP qop, + Algorithm algorithm, + boolean stale) { + this.realm = realm; + this.nonce = nonce; + this.opaque = opaque; + this.qop = qop; + this.algorithm = algorithm; + this.stale = stale; + this.nc = 0; + } + + public int incrementCounter() { + return ++nc; + } + + public String getNonce() { + return nonce; + } + + public String getRealm() { + return realm; + } + + public String getOpaque() { + return opaque; + } + + public Algorithm getAlgorithm() { + return algorithm; + } + + public QOP getQop() { + return qop; + } + + public boolean isStale() { + return stale; + } + + public int getNc() { + return nc; + } + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyFilter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyFilter.java new file mode 100644 index 0000000..c0f74bd --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ProxyFilter.java
@@ -0,0 +1,170 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.List; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class ProxyFilter extends Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> { + + private final ConnectorConfiguration.ProxyConfiguration proxyConfiguration; + private final ProxyDigestAuthenticator proxyDigestAuthenticator = new ProxyDigestAuthenticator(); + private volatile State state = State.CONNECTING; + private volatile InetSocketAddress originalDestinationAddress; + + /** + * Constructor. + * + * @param downstreamFilter downstream filter. Accessible directly as {@link #downstreamFilter} protected field. + */ + ProxyFilter(final Filter<HttpRequest, HttpResponse, ?, ?> downstreamFilter, + ConnectorConfiguration.ProxyConfiguration proxyConfiguration) { + super(downstreamFilter); + this.proxyConfiguration = proxyConfiguration; + } + + @Override + void connect(final SocketAddress address, final Filter<?, ?, HttpRequest, HttpResponse> upstreamFilter) { + this.upstreamFilter = upstreamFilter; + this.originalDestinationAddress = (InetSocketAddress) address; + downstreamFilter.connect(new InetSocketAddress(proxyConfiguration.getHost(), proxyConfiguration.getPort()), this); + } + + @Override + void onConnect() { + HttpRequest connect = createConnectRequest(); + downstreamFilter.write(connect, new CompletionHandler<HttpRequest>() { + @Override + public void failed(final Throwable throwable) { + upstreamFilter.processError(throwable); + } + }); + } + + @Override + boolean processRead(HttpResponse httpResponse) { + if (state == State.CONNECTED) { + // if we have stop the connection phase, just pass through + return true; + } + + switch (httpResponse.getStatusCode()) { + + case 200: { + state = State.CONNECTED; + upstreamFilter.onConnect(); + break; + } + + case 407: { + if (state == State.AUTHENTICATED) { + upstreamFilter.onError(new ProxyAuthenticationException(LocalizationMessages.PROXY_407_TWICE())); + return false; + } + + try { + state = State.AUTHENTICATED; + HttpRequest authenticatingRequest = createAuthenticatingRequest(httpResponse); + downstreamFilter.write(authenticatingRequest, new CompletionHandler<HttpRequest>() { + @Override + public void failed(final Throwable throwable) { + upstreamFilter.processError(throwable); + } + }); + } catch (ProxyAuthenticationException e) { + handleError(e); + return false; + } + + break; + } + + default: { + handleError(new IOException(LocalizationMessages.PROXY_CONNECT_FAIL(httpResponse.getStatusCode()))); + } + } + + return false; + } + + @Override + void write(final HttpRequest data, final CompletionHandler<HttpRequest> completionHandler) { + downstreamFilter.write(data, completionHandler); + } + + private void handleError(Throwable t) { + upstreamFilter.onError(t); + } + + private HttpRequest createAuthenticatingRequest(HttpResponse httpResponse) throws ProxyAuthenticationException { + String authenticateHeader = null; + final List<String> authHeader = httpResponse.getHeader(Constants.PROXY_AUTHENTICATE); + if (authHeader != null && !authHeader.isEmpty()) { + authenticateHeader = authHeader.get(0); + } + + if (authenticateHeader == null || authenticateHeader.equals("")) { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_MISSING_AUTH_HEADER()); + } + + final String[] tokens = authenticateHeader.trim().split("\\s+", 2); + final String scheme = tokens[0]; + + String authorizationHeader; + if (Constants.BASIC.equals(scheme)) { + authorizationHeader = ProxyBasicAuthenticator + .generateAuthorizationHeader(proxyConfiguration.getUserName(), proxyConfiguration.getPassword()); + } else if (Constants.DIGEST.equals(scheme)) { + String originalDestinationUri = getOriginalDestinationUri(); + URI uri = URI.create(originalDestinationUri); + authorizationHeader = proxyDigestAuthenticator + .generateAuthorizationHeader(uri, Constants.CONNECT, authenticateHeader, + proxyConfiguration.getUserName(), proxyConfiguration.getPassword()); + } else { + throw new ProxyAuthenticationException(LocalizationMessages.PROXY_UNSUPPORTED_SCHEME(scheme)); + } + + HttpRequest connectRequest = createConnectRequest(); + connectRequest.addHeaderIfNotPresent(Constants.PROXY_AUTHORIZATION, authorizationHeader); + return connectRequest; + } + + private HttpRequest createConnectRequest() { + String originalDestinationUri = getOriginalDestinationUri(); + URI uri = URI.create(originalDestinationUri); + HttpRequest connect = HttpRequest.createBodyless(Constants.CONNECT, uri); + connect.addHeaderIfNotPresent(Constants.HOST, originalDestinationUri); + connect.addHeaderIfNotPresent(Constants.PROXY_CONNECTION, Constants.KEEP_ALIVE); + return connect; + } + + private String getOriginalDestinationUri() { + return String.format("%s:%d", originalDestinationAddress.getHostString(), originalDestinationAddress.getPort()); + } + + enum State { + CONNECTING, + AUTHENTICATED, + CONNECTED + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ReadListener.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ReadListener.java new file mode 100644 index 0000000..5292732 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ReadListener.java
@@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; + +/** + * <p> + * This class represents a call-back mechanism that will notify implementations + * as HTTP request data becomes available to be read without blocking. + * </p> + * <p/> + * Taken from Servlet 3.1 + */ +interface ReadListener { + + /** + * When an instance of the <code>ReadListener</code> is registered with a {@link BodyInputStream}, + * this method will be invoked by the container the first time when it is possible + * to read data. Subsequently the container will invoke this method if and only + * if {@link BodyInputStream#isReady()} method + * has been called and has returned <code>false</code>. + * + * @throws IOException if an I/O related error has occurred during processing + */ + void onDataAvailable() throws IOException; + + /** + * Invoked when all data for the current request has been read. + * + * @throws IOException if an I/O related error has occurred during processing + */ + + void onAllDataRead() throws IOException; + + /** + * Invoked when an error occurs processing the request. + */ + void onError(Throwable t); +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/RedirectException.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/RedirectException.java new file mode 100644 index 0000000..28c8c65 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/RedirectException.java
@@ -0,0 +1,64 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import org.glassfish.jersey.client.ClientProperties; + +/** + * This Exception is used only if {@link ClientProperties#FOLLOW_REDIRECTS} is set to {@code true}. + * <p/> + * This exception is thrown when any of the Redirect HTTP response status codes (301, 302, 303, 307, 308) is received and: + * <ul> + * <li> + * the chained redirection count exceeds the value of + * {@link org.glassfish.jersey.client.JdkConnectorProvider#MAX_REDIRECTS} + * </li> + * <li> + * or an infinite redirection loop is detected + * </li> + * <li> + * or Location response header is missing, empty or does not contain a valid {@link java.net.URI}. + * </li> + * </ul> + * + * @author Ondrej Kosatka (ondrej.kosatka at oracle.com) + * @see RedirectHandler + */ +public class RedirectException extends Exception { + + private static final long serialVersionUID = 4357724300486801294L; + + /** + * Constructor. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public RedirectException(String message) { + super(message); + } + + /** + * Constructor. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public RedirectException(String message, Throwable t) { + super(message, t); + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/RedirectHandler.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/RedirectHandler.java new file mode 100644 index 0000000..79e1973 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/RedirectHandler.java
@@ -0,0 +1,187 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + * @author Ondrej Kosatka (ondrej.kosatka at oracle.com) + */ +class RedirectHandler { + + private static final Set<Integer> REDIRECT_STATUS_CODES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(301, 302, 303, 307, 308))); + + private final int maxRedirects; + private final boolean followRedirects; + private final Set<URI> redirectUriHistory; + private final HttpConnectionPool httpConnectionPool; + private final HttpRequest originalHttpRequest; + + private volatile URI lastRequestUri = null; + + RedirectHandler(HttpConnectionPool httpConnectionPool, HttpRequest originalHttpRequest, + ConnectorConfiguration connectorConfiguration) { + this.followRedirects = connectorConfiguration.getFollowRedirects(); + this.maxRedirects = connectorConfiguration.getMaxRedirects(); + this.httpConnectionPool = httpConnectionPool; + this.originalHttpRequest = originalHttpRequest; + this.redirectUriHistory = new HashSet<>(maxRedirects); + this.lastRequestUri = originalHttpRequest.getUri(); + } + + void handleRedirects(final HttpResponse httpResponse, final CompletionHandler<HttpResponse> completionHandler) { + if (!followRedirects) { + completionHandler.completed(httpResponse); + return; + } + + if (!REDIRECT_STATUS_CODES.contains(httpResponse.getStatusCode())) { + completionHandler.completed(httpResponse); + return; + } + + if (httpResponse.getStatusCode() != 303) { + // we support other methods than GET and HEAD only with 303 + if (!Constants.HEAD.equals(originalHttpRequest.getMethod()) && !Constants.GET + .equals(originalHttpRequest.getMethod())) { + completionHandler.completed(httpResponse); + return; + } + } + + // reading the body is not necessary, but if we wait until the entire body has arrived, we can reuse the same connection + consumeBodyIfPresent(httpResponse, new CompletionHandler<Void>() { + @Override + public void failed(Throwable throwable) { + completionHandler.failed(throwable); + } + + @Override + public void completed(Void r) { + doRedirect(httpResponse, new CompletionHandler<HttpResponse>() { + @Override + public void failed(Throwable throwable) { + completionHandler.failed(throwable); + } + + @Override + public void completed(HttpResponse result) { + handleRedirects(result, completionHandler); + } + }); + } + }); + } + + private void doRedirect(final HttpResponse httpResponse, final CompletionHandler<HttpResponse> completionHandler) { + + // get location header + String locationString = null; + final List<String> locationHeader = httpResponse.getHeader("Location"); + if (locationHeader != null && !locationHeader.isEmpty()) { + locationString = locationHeader.get(0); + } + + if (locationString == null || locationString.isEmpty()) { + completionHandler.failed(new RedirectException(LocalizationMessages.REDIRECT_NO_LOCATION())); + return; + } + + URI location; + try { + location = new URI(locationString); + + if (!location.isAbsolute()) { + // location is not absolute, we need to resolve it. + URI baseUri = lastRequestUri; + location = baseUri.resolve(location.normalize()); + } + } catch (URISyntaxException e) { + completionHandler.failed(new RedirectException(LocalizationMessages.REDIRECT_ERROR_DETERMINING_LOCATION(), e)); + return; + } + + // infinite loop detection + boolean alreadyRequested = !redirectUriHistory.add(location); + if (alreadyRequested) { + completionHandler.failed(new RedirectException(LocalizationMessages.REDIRECT_INFINITE_LOOP())); + return; + } + + // maximal number of redirection + if (redirectUriHistory.size() > maxRedirects) { + completionHandler.failed(new RedirectException(LocalizationMessages.REDIRECT_LIMIT_REACHED(maxRedirects))); + return; + } + + String method = originalHttpRequest.getMethod(); + Map<String, List<String>> headers = originalHttpRequest.getHeaders(); + if (httpResponse.getStatusCode() == 303 && !method.equals(Constants.HEAD)) { + // in case of 303 we rewrite every method except HEAD to GET + method = Constants.GET; + // remove entity-transport headers if present + headers.remove(Constants.CONTENT_LENGTH); + headers.remove(Constants.TRANSFER_ENCODING_HEADER); + } + + HttpRequest httpRequest = HttpRequest.createBodyless(method, location); + httpRequest.getHeaders().putAll(headers); + lastRequestUri = location; + + httpConnectionPool.send(httpRequest, completionHandler); + } + + private void consumeBodyIfPresent(HttpResponse response, final CompletionHandler<Void> completionHandler) { + final AsynchronousBodyInputStream bodyStream = response.getBodyStream(); + bodyStream.setReadListener(new ReadListener() { + @Override + public void onDataAvailable() { + while (bodyStream.isReady()) { + try { + bodyStream.read(); + } catch (IOException e) { + completionHandler.failed(e); + } + } + } + + @Override + public void onAllDataRead() { + completionHandler.completed(null); + } + + @Override + public void onError(Throwable t) { + completionHandler.failed(t); + } + }); + } + + URI getLastRequestUri() { + return lastRequestUri; + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java new file mode 100644 index 0000000..d463c4a --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java
@@ -0,0 +1,709 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Queue; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; + + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class SslFilter extends Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> { + +/* SYNCHRONIZATION NOTE: SSLEngine#wrap and SSLEngine#unwrap can be done concurrently (one thread doing wrap + and another doing unwrap). The same operation cannot be done concurrently (2 threads doing wrap). + + Method doHandshakeStep must be synchronized, because it might be entered both by writing and reading thread + during re-handshake. Write, close and re-handshake cannot be done concurrently, because all those operations might + do SSLEngine#wrap. Read can be be done concurrently with any other operation, because even thought re-handshake + can do SSLEngine#unwrap, it won't do so if it was entered from write operation. + + Operations upstreamFilter#onRead cannot be done while holding a lock of this class. Doing so might lead to a deadlock. An + example of deadlock would be if a thread holding a lock in upstreamFilter#onRead writes a response synchronously (blocks + and waits for write completion handler). The write completion handler might be executed by another thread which will not be + able to obtain a lock for this class.*/ + + /* Some operations on SSL engine require a buffer as a parameter even if they don't need any data. + This buffer is for that purpose. */ + private static final ByteBuffer emptyBuffer = ByteBuffer.allocate(0); + + // buffer for passing data to the upper filter + private final ByteBuffer applicationInputBuffer; + // buffer for passing data to the transport filter + private final ByteBuffer networkOutputBuffer; + private final SSLEngine sslEngine; + private final HostnameVerifier customHostnameVerifier; + private final String serverHost; + private final WriteQueue writeQueue = new WriteQueue(); + + private volatile State state = State.NOT_STARTED; + /* + * Pending write operation stored when writing data was not possible. It will be resumed when write operation is + * available again. Only one write operation can be in progress at a time. Trying to store more than one pending + * application write indicates that an upper stack called write without waiting for the completion handler + * of the previous write. + * Currently this is used only during re-handshake. + */ + private Runnable pendingApplicationWrite = null; + + /** + * SSL Filter constructor, takes upstream filter as a parameter. + * + * @param downstreamFilter a filter that is positioned under the SSL filter. + * @param sslContext configuration of SSL engine. + * @param serverHost server host (hostname or IP address), which will be used to verify authenticity of + * the server (the provided host will be compared against the host in the certificate + * provided by the server). IP address and hostname cannot be used interchangeably - + * if a certificate contains hostname and an IP address of the server is provided here, + * the verification will fail. + * @param customHostnameVerifier hostname verifier that will be used instead of the default one. + */ + SslFilter(Filter<ByteBuffer, ByteBuffer, ?, ?> downstreamFilter, + SSLContext sslContext, + String serverHost, + HostnameVerifier customHostnameVerifier) { + super(downstreamFilter); + this.serverHost = serverHost; + sslEngine = sslContext.createSSLEngine(serverHost, -1); + sslEngine.setUseClientMode(true); + this.customHostnameVerifier = customHostnameVerifier; + + /** + * Enable server host verification. + * This can be moved to {@link SslEngineConfigurator} with the rest of {@link SSLEngine} configuration + * when {@link SslEngineConfigurator} supports Java 7. + */ + if (customHostnameVerifier == null) { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + sslEngine.setSSLParameters(sslParameters); + } + + applicationInputBuffer = ByteBuffer.allocate(sslEngine.getSession().getApplicationBufferSize()); + networkOutputBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize()); + } + + @Override + synchronized void write(final ByteBuffer applicationData, final CompletionHandler<ByteBuffer> completionHandler) { + switch (state) { + // before SSL is started, write just passes through + case NOT_STARTED: { + writeQueue.write(applicationData, completionHandler); + return; + } + + /* TODO: + The current model does not permit calling write before SSL handshake has completed, if we allow this + we could easily get rid of the onSslHandshakeCompleted event. The SSL filter can simply store the write until + the handshake has completed like during re-handshake. With such a change HANDSHAKING and REHANDSHAKING could + be collapsed into one state. */ + case HANDSHAKING: { + completionHandler.failed(new IllegalStateException("Cannot write until SSL handshake has been completed")); + break; + } + + /* Suspend all writes until the re-handshaking is done. Data are permitted during re-handshake in SSL, but this + would only complicate things */ + case REHANDSHAKING: { + storePendingApplicationWrite(applicationData, completionHandler); + break; + } + + case DATA: { + handleWrite(applicationData, completionHandler); + break; + } + + case CLOSED: { + // the engine is closed just abort with failure + completionHandler.failed(new IllegalStateException(LocalizationMessages.SSL_SESSION_CLOSED())); + break; + } + } + } + + private void handleWrite(final ByteBuffer applicationData, final CompletionHandler<ByteBuffer> completionHandler) { + try { + // transport buffer always writes all data, so there are not leftovers in the networkOutputBuffer + networkOutputBuffer.clear(); + SSLEngineResult result = sslEngine.wrap(applicationData, networkOutputBuffer); + + switch (result.getStatus()) { + case BUFFER_OVERFLOW: { + /* this means that the content of the ssl packet (max 16kB) did not fit into + networkOutputBuffer, we make sure to set networkOutputBuffer > max 16kB + SSL headers + when initializing this filter. This indicates a bug. */ + throw new IllegalStateException("SSL packet does not fit into the network buffer: " + + networkOutputBuffer + "\n" + getDebugState()); + } + + case BUFFER_UNDERFLOW: { + /* This basically says that there is not enough data to create an SSL packet. Javadoc suggests that + BUFFER_UNDERFLOW can occur only after unwrap(), but to be 100% sure we handle all possible error states: */ + throw new IllegalStateException("SSL engine underflow with the following application input: " + + applicationData + "\n" + getDebugState()); + } + + case CLOSED: { + state = State.CLOSED; + break; + } + + case OK: { + // check if we started re-handshaking + if (result.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + state = State.REHANDSHAKING; + } + + networkOutputBuffer.flip(); + // write only if something was written to the output buffer + if (networkOutputBuffer.hasRemaining()) { + writeQueue.write(networkOutputBuffer, new CompletionHandler<ByteBuffer>() { + @Override + public void completed(ByteBuffer result) { + handlePostWrite(applicationData, completionHandler); + } + + @Override + public void failed(Throwable throwable) { + completionHandler.failed(throwable); + } + }); + } else { + handlePostWrite(applicationData, completionHandler); + } + break; + } + } + + } catch (SSLException e) { + handleSslError(e); + } + } + + private synchronized void handlePostWrite(final ByteBuffer applicationData, + final CompletionHandler<ByteBuffer> completionHandler) { + if (state == State.REHANDSHAKING) { + if (applicationData.hasRemaining()) { + // the remaining data will be sent after re-handshake + storePendingApplicationWrite(applicationData, completionHandler); + // start re-handshaking + doHandshakeStep(emptyBuffer); + } + } else { + if (applicationData.hasRemaining()) { + // make sure to empty the application output buffer + handleWrite(applicationData, completionHandler); + } else { + completionHandler.completed(applicationData); + } + } + } + + private void storePendingApplicationWrite(final ByteBuffer applicationData, + final CompletionHandler<ByteBuffer> completionHandler) { + // store the write until re-handshaking is completed + if (pendingApplicationWrite != null) { + /* If this happens it means a bug in this class or upper layer called another write() without waiting + for a completion handler of the previous one. */ + throw new IllegalStateException("Only one write operation can be in progress\n" + getDebugState()); + } + + pendingApplicationWrite = () -> { + // go again through the entire write procedure like this data came directly from the application + write(applicationData, completionHandler); + }; + } + + @Override + synchronized void close() { + if (state == State.NOT_STARTED) { + downstreamFilter.close(); + return; + } + + sslEngine.closeOutbound(); + try { + LazyBuffer lazyBuffer = new LazyBuffer(); + + while (sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) { + ByteBuffer buffer = lazyBuffer.get(); + SSLEngineResult result = sslEngine.wrap(emptyBuffer, buffer); + + switch (result.getStatus()) { + case BUFFER_OVERFLOW: { + lazyBuffer.resize(); + break; + } + + case BUFFER_UNDERFLOW: { + /* This basically says that there is not enough data to create an SSL packet. Javadoc suggests that + BUFFER_UNDERFLOW can occur only after unwrap(), but to be 100% sure we handle all possible error + states: */ + throw new IllegalStateException("SSL engine underflow while close operation \n" + getDebugState()); + } + + // CLOSE or OK are expected outcomes + } + + } + + if (lazyBuffer.isAllocated()) { + ByteBuffer buffer = lazyBuffer.get(); + buffer.flip(); + writeQueue.write(buffer, new CompletionHandler<ByteBuffer>() { + + @Override + public void completed(ByteBuffer result) { + downstreamFilter.close(); + } + + @Override + public void failed(Throwable throwable) { + downstreamFilter.close(); + } + }); + } else { + // make sure we close even if SSL had nothing to send + downstreamFilter.close(); + } + } catch (Exception e) { + handleSslError(e); + } + } + + @Override + boolean processRead(ByteBuffer networkData) { + /* A flag indicating if we should keep reading from the network buffer. + If false, the buffer contains an uncompleted packet -> stop reading, SSL engine accepts only whole packets */ + boolean readMore = true; + + while (networkData.hasRemaining() && readMore) { + switch (state) { + // before SSL is started write just passes through + case NOT_STARTED: { + return true; + } + + case HANDSHAKING: + case REHANDSHAKING: { + readMore = doHandshakeStep(networkData); + break; + } + + case DATA: { + readMore = handleRead(networkData); + break; + } + + case CLOSED: { + // drop any data that arrive after the SSL has been closed + networkData.clear(); + readMore = false; + } + } + } + + return false; + } + + private boolean handleRead(ByteBuffer networkData) { + try { + applicationInputBuffer.clear(); + SSLEngineResult result = sslEngine.unwrap(networkData, applicationInputBuffer); + + switch (result.getStatus()) { + case BUFFER_OVERFLOW: { + /* This means that the content of the ssl packet (max 16kB) did not fit into + applicationInputBuffer, but we make sure to set applicationInputBuffer > max 16kB + when initializing this filter. This indicates a bug.*/ + throw new IllegalStateException("Contents of a SSL packet did not fit into buffer: " + + applicationInputBuffer + "\n" + getDebugState()); + } + + case BUFFER_UNDERFLOW: { + // the ssl packet is not full, return and indicate that we won't get more from this buffer + return false; + } + + case CLOSED: + case OK: { + if (result.bytesProduced() > 0) { + applicationInputBuffer.flip(); + upstreamFilter.onRead(applicationInputBuffer); + applicationInputBuffer.compact(); + } + + if (sslEngine.isInboundDone()) { + /* we have just received a close alert from our peer, so we are done. If there is something + remaining in the input buffer, just drop it. */ + + // signal that there is nothing useful left in this buffer + return false; + } + + // we started re-handshaking + if (result.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING + // make sure we don't confuse re-handshake with closing handshake + && !sslEngine.isOutboundDone()) { + state = State.REHANDSHAKING; + return doHandshakeStep(networkData); + } + + break; + } + } + } catch (SSLException e) { + handleSslError(e); + } + + return true; + } + + private boolean doHandshakeStep(ByteBuffer networkData) { + /* Buffer used to store application data read during this handshake step. + Application data can be interleaved with handshake messages only during re-handshake. + We don't use applicationInputBuffer, because we might want to store more than one packet */ + LazyBuffer inputBuffer = new LazyBuffer(); + boolean handshakeFinished = false; + + synchronized (this) { + if (SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING.equals(sslEngine.getHandshakeStatus())) { + // we stopped handshaking while waiting for the lock + return true; + } + + try { + /* we don't use networkOutputBuffer, because there might be a write operation still in progress -> + we don't want to corrupt the buffer it is using */ + LazyBuffer outputBuffer = new LazyBuffer(); + boolean stepFinished = false; + while (!stepFinished) { + SSLEngineResult.HandshakeStatus hs = sslEngine.getHandshakeStatus(); + + switch (hs) { + case NOT_HANDSHAKING: { + /* This should never happen. If we are here and not handshaking, it means a bug + in the state machine of this class, because we stopped handshaking and did not exit this while loop. + The could be caused either by overlooking FINISHED state or incorrectly treating an error. */ + + throw new IllegalStateException("Trying to handshake, but SSL engine not in HANDSHAKING state." + + "SSL filter state: \n" + getDebugState()); + } + + case FINISHED: { + /* According to SSLEngine javadoc FINISHED status can be returned only in SSLEngineResult, + but just to make sure we don't end up in an infinite loop when presented with an SSLEngine + implementation that does not respect this:*/ + stepFinished = true; + handshakeFinished = true; + break; + } + // needs to write data to the network + case NEED_WRAP: { + ByteBuffer byteBuffer = outputBuffer.get(); + SSLEngineResult result = sslEngine.wrap(emptyBuffer, byteBuffer); + + if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED) { + stepFinished = true; + handshakeFinished = true; + } + + switch (result.getStatus()) { + case BUFFER_OVERFLOW: { + outputBuffer.resize(); + break; + } + + case BUFFER_UNDERFLOW: { + /* This basically says that there is not enough data to create an SSL packet. Javadoc suggests + that BUFFER_UNDERFLOW can occur only after unwrap(), but to be 100% sure we handle all + possible error states: */ + throw new IllegalStateException("SSL engine underflow with the following SSL filter " + + "state: \n" + getDebugState()); + } + + case CLOSED: { + stepFinished = true; + state = State.CLOSED; + break; + } + } + + break; + } + + case NEED_UNWRAP: { + + SSLEngineResult result = sslEngine.unwrap(networkData, applicationInputBuffer); + + applicationInputBuffer.flip(); + if (applicationInputBuffer.hasRemaining()) { + // data can flow during re-handshake + inputBuffer.append(applicationInputBuffer); + } + applicationInputBuffer.compact(); + + if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED) { + stepFinished = true; + handshakeFinished = true; + } + + switch (result.getStatus()) { + case BUFFER_OVERFLOW: { + /* This means that the content of the ssl packet (max 16kB) did not fit into + applicationInputBuffer, but we make sure to set applicationInputBuffer > max 16kB + when initializing this filter. This indicates a bug. */ + throw new IllegalStateException("SSL packet does not fit into the network buffer: " + + getDebugState()); + } + + case BUFFER_UNDERFLOW: { + // indicate that we won't get more from this buffer + stepFinished = true; + break; + } + + case CLOSED: { + stepFinished = true; + state = State.CLOSED; + break; + } + } + + break; + } + // needs to execute long running task (for instance validating certificates) + case NEED_TASK: { + Runnable delegatedTask; + while ((delegatedTask = sslEngine.getDelegatedTask()) != null) { + delegatedTask.run(); + } + break; + } + } + } + + // now write the stored wrap() results + if (outputBuffer.isAllocated()) { + ByteBuffer buffer = outputBuffer.get(); + buffer.flip(); + writeQueue.write(buffer, null); + } + + } catch (Exception e) { + handleSslError(e); + } + } + + /* Handle any read data. + We have to execute upstreamFilter.onRead after releasing the lock. See the synchronization note on top. + Only one read operation can be in progress at a time, so even though we have released the lock, no other + SSlEngine#unwrap can be performed until this method returns. So there is no chance of the read data + being mixed up */ + if (inputBuffer.isAllocated()) { + ByteBuffer buffer = inputBuffer.get(); + upstreamFilter.onRead(buffer); + } + + if (handshakeFinished) { + handleHandshakeFinished(); + // indicate that there still might be usable data in the input buffer + return true; + } + + /* if we are here, it means that we are waiting for more data -> indicate that there is nothing usable in the + input buffer left */ + return false; + } + + private void handleHandshakeFinished() { + // Apply a custom host verifier if present. Do it for both handshaking and re-handshaking. + if (customHostnameVerifier != null && !customHostnameVerifier.verify(serverHost, sslEngine.getSession())) { + handleSslError(new SSLException("Server host name verification using " + customHostnameVerifier + .getClass() + " has failed")); + return; + } + + if (state == State.HANDSHAKING) { + state = State.DATA; + upstreamFilter.onSslHandshakeCompleted(); + } else if (state == State.REHANDSHAKING) { + state = State.DATA; + if (pendingApplicationWrite != null) { + Runnable write = pendingApplicationWrite; + // set pending write to null to cover the extremely improbable case that we start re-handshaking again + pendingApplicationWrite = null; + + write.run(); + } + } + } + + private void handleSslError(Throwable t) { + onError(t); + } + + @Override + void startSsl() { + try { + state = State.HANDSHAKING; + sslEngine.beginHandshake(); + doHandshakeStep(emptyBuffer); + } catch (SSLException e) { + handleSslError(e); + } + } + + /** + * Only for test. + */ + void rehandshake() { + try { + sslEngine.beginHandshake(); + } catch (SSLException e) { + handleSslError(e); + } + } + + /** + * Returns a printed current state of the SslFilter that could be helpful for troubleshooting. + */ + private String getDebugState() { + return "SslFilter{" + + "\napplicationInputBuffer=" + applicationInputBuffer + + ",\nnetworkOutputBuffer=" + networkOutputBuffer + + ",\nsslEngineStatus=" + sslEngine.getHandshakeStatus() + + ",\nsslSession=" + sslEngine.getSession() + + ",\nstate=" + state + + ",\npendingApplicationWrite=" + pendingApplicationWrite + + ",\npendingWritesSize=" + writeQueue + + '}'; + } + + private enum State { + NOT_STARTED, + HANDSHAKING, + REHANDSHAKING, + DATA, + CLOSED + } + + private class LazyBuffer { + + private ByteBuffer buffer = null; + + ByteBuffer get() { + if (buffer == null) { + buffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize()); + } + + return buffer; + } + + boolean isAllocated() { + return buffer != null; + } + + void resize() { + int increment = sslEngine.getSession().getPacketBufferSize(); + int newSize = buffer.position() + increment; + ByteBuffer newBuffer = ByteBuffer.allocate(newSize); + buffer.flip(); + newBuffer.flip(); + buffer = Utils.appendBuffers(newBuffer, buffer, newBuffer.limit(), 50); + buffer.compact(); + } + + void append(ByteBuffer b) { + if (buffer == null) { + buffer = ByteBuffer.allocate(b.remaining()); + buffer.flip(); + } + int newSize = buffer.limit() + b.remaining(); + buffer = Utils.appendBuffers(buffer, b, newSize, 50); + } + } + + // synchronized on the outer class, because there is a danger of deadlock if this has its own lock + private class WriteQueue { + + private final Queue<Runnable> pendingWrites = new LinkedList<>(); + + void write(final ByteBuffer data, final CompletionHandler<ByteBuffer> completionHandler) { + synchronized (SslFilter.this) { + Runnable r = () -> downstreamFilter.write(data, new CompletionHandler<ByteBuffer>() { + + @Override + public void completed(ByteBuffer result) { + if (completionHandler != null) { + completionHandler.completed(result); + } + + onWriteCompleted(); + } + + @Override + public void failed(Throwable throwable) { + if (completionHandler != null) { + completionHandler.failed(throwable); + } + + onWriteCompleted(); + } + }); + + pendingWrites.offer(r); + // if our task is the first one in the queue, there is no other write task in progress -> process it + if (pendingWrites.peek() == r) { + r.run(); + } + } + } + + private void onWriteCompleted() { + synchronized (SslFilter.this) { + // task in progress is at the head of the queue -> remove it + pendingWrites.poll(); + Runnable next = pendingWrites.peek(); + + if (next != null) { + next.run(); + } + } + } + + @Override + public String toString() { + synchronized (SslFilter.this) { + return "WriteQueue{" + + "pendingWrites=" + + pendingWrites.size() + + '}'; + } + } + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ThreadPoolConfig.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ThreadPoolConfig.java new file mode 100644 index 0000000..0b02290 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ThreadPoolConfig.java
@@ -0,0 +1,453 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.util.Queue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * Client thread pool configuration, which might be used to customize client thread pool. + * <p/> + * One can get a default <tt>ThreadPoolConfig</tt> using {@link ThreadPoolConfig#defaultConfig()} + * and customize it according to the application specific requirements. + * <p/> + * A <tt>ThreadPoolConfig</tt> object might be customized in a "Builder"-like fashion: + * <pre> + * ThreadPoolConfig.defaultConfig() + * .setPoolName("App1Pool") + * .setCorePoolSize(5) + * .setMaxPoolSize(10); + * </pre> + * + * @author Oleksiy Stashok + * @author gustav trede + */ +public final class ThreadPoolConfig { + + private static final int DEFAULT_CORE_POOL_SIZE = 1; + private static final int DEFAULT_MAX_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 50); + private static final int DEFAULT_MAX_QUEUE_SIZE = -1; + private static final int DEFAULT_IDLE_THREAD_KEEP_ALIVE_TIMEOUT = 10; + + private static final ThreadPoolConfig DEFAULT = new ThreadPoolConfig( + "jdk-connector", DEFAULT_CORE_POOL_SIZE, + DEFAULT_MAX_POOL_SIZE, + null, DEFAULT_MAX_QUEUE_SIZE, + DEFAULT_IDLE_THREAD_KEEP_ALIVE_TIMEOUT, + TimeUnit.SECONDS, + null, Thread.NORM_PRIORITY, true, null); + + /** + * Create new client thread pool configuration instance. The returned <tt>ThreadPoolConfig</tt> instance will be + * pre-configured with a default values. + * + * @return client thread pool configuration instance. + */ + public static ThreadPoolConfig defaultConfig() { + return DEFAULT.copy(); + } + + private String poolName; + private int corePoolSize; + private int maxPoolSize; + private Queue<Runnable> queue; + private int queueLimit = -1; + private long keepAliveTimeMillis; + private ThreadFactory threadFactory; + private int priority = Thread.MAX_PRIORITY; + private boolean isDaemon; + private ClassLoader initialClassLoader; + + private ThreadPoolConfig(String poolName, + int corePoolSize, + int maxPoolSize, + Queue<Runnable> queue, + int queueLimit, + long keepAliveTime, + TimeUnit timeUnit, + ThreadFactory threadFactory, + int priority, + boolean isDaemon, + ClassLoader initialClassLoader) { + this.poolName = poolName; + this.corePoolSize = corePoolSize; + this.maxPoolSize = maxPoolSize; + this.queue = queue; + this.queueLimit = queueLimit; + if (keepAliveTime > 0) { + this.keepAliveTimeMillis = + TimeUnit.MILLISECONDS.convert(keepAliveTime, timeUnit); + } else { + keepAliveTimeMillis = keepAliveTime; + } + + this.threadFactory = threadFactory; + this.priority = priority; + this.isDaemon = isDaemon; + this.initialClassLoader = initialClassLoader; + } + + private ThreadPoolConfig(ThreadPoolConfig cfg) { + this.queue = cfg.queue; + this.threadFactory = cfg.threadFactory; + this.poolName = cfg.poolName; + this.priority = cfg.priority; + this.isDaemon = cfg.isDaemon; + this.maxPoolSize = cfg.maxPoolSize; + this.queueLimit = cfg.queueLimit; + this.corePoolSize = cfg.corePoolSize; + this.keepAliveTimeMillis = cfg.keepAliveTimeMillis; + this.initialClassLoader = cfg.initialClassLoader; + } + + /** + * Return a copy of this thread pool config. + * + * @return a copy of this thread pool config. + */ + public ThreadPoolConfig copy() { + return new ThreadPoolConfig(this); + } + + /** + * Return a queue that will be used to temporarily store tasks when all threads in the thread pool are busy. + * + * @return queue that will be used to temporarily store tasks when all threads in the thread pool are busy. + */ + public Queue<Runnable> getQueue() { + return queue; + } + + /** + * Set a queue implementation that will be used to temporarily store tasks when all threads in the thread pool are busy. + * + * @param queue queue implementation that will be used to temporarily store tasks when all threads in the thread pool are + * busy. + * @return the {@link ThreadPoolConfig} with the new {@link Queue} implementation. + */ + public ThreadPoolConfig setQueue(Queue<Runnable> queue) { + this.queue = queue; + return this; + } + + /** + * Return {@link ThreadFactory} that will be used to create thread pool threads. + * <p/> + * If {@link ThreadFactory} is set, then {@link #priority}, {@link #isDaemon}, + * {@link #poolName} settings will not be considered when creating new threads. + * + * @return {@link ThreadFactory} that will be used to create thread pool threads. + */ + public ThreadFactory getThreadFactory() { + return threadFactory; + } + + /** + * Set {@link ThreadFactory} that will be used to create thread pool threads. + * + * @param threadFactory custom {@link ThreadFactory} + * If {@link ThreadFactory} is set, then {@link #priority}, {@link #isDaemon}, + * {@link #poolName} settings will not be considered when creating new threads. + * @return the {@link ThreadPoolConfig} with the new {@link ThreadFactory} + */ + public ThreadPoolConfig setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + return this; + } + + /** + * Return thread pool name. The default is "Tyrus-client". + * + * @return the thread pool name. + */ + public String getPoolName() { + return poolName; + } + + /** + * Set thread pool name. The default is "Tyrus-client". + * + * @param poolName the thread pool name. + * @return the {@link ThreadPoolConfig} with the new thread pool name. + */ + public ThreadPoolConfig setPoolName(String poolName) { + this.poolName = poolName; + return this; + } + + /** + * Get priority of the threads in thread pool. The default is {@link Thread#NORM_PRIORITY}. + * + * @return priority of the threads in thread pool. + */ + public int getPriority() { + return priority; + } + + /** + * Set priority of the threads in thread pool. The default is {@link Thread#NORM_PRIORITY}. + * + * @param priority of the threads in thread pool. + * @return the {@link ThreadPoolConfig} with the new thread priority. + */ + public ThreadPoolConfig setPriority(int priority) { + this.priority = priority; + return this; + } + + /** + * Return {@code true} if thread pool threads are daemons. The default is {@code true}. + * + * @return {@code true} if thread pool threads are daemons. + */ + public boolean isDaemon() { + return isDaemon; + } + + /** + * Set {@code true} if thread pool threads are daemons. The default is {@code true}. + * + * @param isDaemon {@code true} if thread pool threads are daemons. + * @return the {@link ThreadPoolConfig} with the daemon property set. + */ + public ThreadPoolConfig setDaemon(boolean isDaemon) { + this.isDaemon = isDaemon; + return this; + } + + /** + * Get max thread pool size. The default is {@code Math.max(Runtime.getRuntime().availableProcessors(), 20)} + * + * @return max thread pool size. + */ + public int getMaxPoolSize() { + return maxPoolSize; + } + + /** + * Set max thread pool size. The default is The default is {@code Math.max(Runtime.getRuntime().availableProcessors(), 20)}. + * <p/> + * Cannot be smaller than 3. + * + * @param maxPoolSize the max thread pool size. + * @return the {@link ThreadPoolConfig} with the new max pool size set. + */ + public ThreadPoolConfig setMaxPoolSize(int maxPoolSize) { + if (maxPoolSize < 3) { + throw new IllegalArgumentException(LocalizationMessages.THREAD_POOL_MAX_SIZE_TOO_SMALL()); + } + + this.maxPoolSize = maxPoolSize; + return this; + } + + /** + * Get the core thread pool size - the size of the thread pool will never bee smaller than this. + * <p/> + * The default is 1. + * + * @return the core thread pool size - the size of the thread pool will never bee smaller than this. + */ + public int getCorePoolSize() { + return corePoolSize; + } + + /** + * Set the core thread pool size - the size of the thread pool will never bee smaller than this. + * <p/> + * The default is 1. + * + * @param corePoolSize the core thread pool size - the size of the thread pool will never bee smaller than this. + * @return the {@link ThreadPoolConfig} with the new core pool size set. + */ + public ThreadPoolConfig setCorePoolSize(int corePoolSize) { + if (corePoolSize < 0) { + throw new IllegalArgumentException(LocalizationMessages.THREAD_POOL_CORE_SIZE_TOO_SMALL()); + } + + this.corePoolSize = corePoolSize; + return this; + } + + /** + * Get the limit of the queue, where tasks are temporarily stored when all threads are busy. + * <p/> + * Value less than 0 means unlimited queue. The default is -1. + * + * @return the thread-pool queue limit. The queue limit + */ + public int getQueueLimit() { + return queueLimit; + } + + /** + * Set the limit of the queue, where tasks are temporarily stored when all threads are busy. + * <p/> + * Value less than 0 means unlimited queue. The default is -1. + * + * @param queueLimit the thread pool queue limit. The <tt>queueLimit</tt> value less than 0 means unlimited queue. + * @return the {@link ThreadPoolConfig} with the new queue limit. + */ + public ThreadPoolConfig setQueueLimit(int queueLimit) { + if (queueLimit < 0) { + this.queueLimit = -1; + } else { + this.queueLimit = queueLimit; + } + return this; + } + + /** + * The max period of time a thread will wait for a new task to process. + * <p/> + * If the timeout expires and the thread is not a core one (see {@link #setCorePoolSize(int)}, {@link #setMaxPoolSize(int)}) + * - then the thread will be terminated and removed from the thread pool. + * <p/> + * The default is 10s. + * + * @param time max keep alive timeout. The value less than 0 means no timeout. + * @param unit time unit. + * @return the {@link ThreadPoolConfig} with the new keep alive time. + */ + public ThreadPoolConfig setKeepAliveTime(long time, TimeUnit unit) { + if (time < 0) { + keepAliveTimeMillis = -1; + } else { + keepAliveTimeMillis = TimeUnit.MILLISECONDS.convert(time, unit); + } + return this; + } + + /** + * Get the max period of time a thread will wait for a new task to process. + * <p/> + * If the timeout expires and the thread is not a core one (see {@link #setCorePoolSize(int)}, {@link #setMaxPoolSize(int)}) + * - then the thread will be terminated and removed from the thread pool. + * <p/> + * The default is 10s. + * + * @return the keep-alive timeout, the value less than 0 means no timeout. + */ + public long getKeepAliveTime(TimeUnit timeUnit) { + if (keepAliveTimeMillis == -1) { + return -1; + } + + return timeUnit.convert(keepAliveTimeMillis, TimeUnit.MILLISECONDS); + } + + /** + * Get the class loader (if any) to be initially exposed by threads from this pool. + * <p/> + * If not specified, the class loader of the parent thread that initialized the pool will be used. + * + * @return the class loader (if any) to be initially exposed by threads from this pool. + */ + public ClassLoader getInitialClassLoader() { + return initialClassLoader; + } + + /** + * Specifies the context class loader that will be used by threads in this pool. + * <p/> + * If not specified, the class loader of the parent thread that initialized the pool will be used. + * + * @param initialClassLoader the class loader to be exposed by threads of this pool. + * @return the {@link ThreadPoolConfig} with the class loader set. + * @see Thread#getContextClassLoader() + */ + public ThreadPoolConfig setInitialClassLoader(final ClassLoader initialClassLoader) { + this.initialClassLoader = initialClassLoader; + return this; + } + + @Override + public String toString() { + return ThreadPoolConfig.class.getSimpleName() + " :\r\n" + + " poolName: " + poolName + "\r\n" + + " corePoolSize: " + corePoolSize + "\r\n" + + " maxPoolSize: " + maxPoolSize + "\r\n" + + " queue: " + (queue != null ? queue.getClass() : "undefined") + "\r\n" + + " queueLimit: " + queueLimit + "\r\n" + + " keepAliveTime (millis): " + keepAliveTimeMillis + "\r\n" + + " threadFactory: " + threadFactory + "\r\n" + + " priority: " + priority + "\r\n" + + " isDaemon: " + isDaemon + "\r\n" + + " initialClassLoader: " + initialClassLoader; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + ThreadPoolConfig that = (ThreadPoolConfig) o; + + if (corePoolSize != that.corePoolSize) { + return false; + } + if (isDaemon != that.isDaemon) { + return false; + } + if (keepAliveTimeMillis != that.keepAliveTimeMillis) { + return false; + } + if (maxPoolSize != that.maxPoolSize) { + return false; + } + if (priority != that.priority) { + return false; + } + if (queueLimit != that.queueLimit) { + return false; + } + if (initialClassLoader != null ? !initialClassLoader.equals(that.initialClassLoader) : that.initialClassLoader != null) { + return false; + } + if (poolName != null ? !poolName.equals(that.poolName) : that.poolName != null) { + return false; + } + if (queue != null ? !queue.equals(that.queue) : that.queue != null) { + return false; + } + if (threadFactory != null ? !threadFactory.equals(that.threadFactory) : that.threadFactory != null) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = poolName != null ? poolName.hashCode() : 0; + result = 31 * result + corePoolSize; + result = 31 * result + maxPoolSize; + result = 31 * result + (queue != null ? queue.hashCode() : 0); + result = 31 * result + queueLimit; + result = 31 * result + (int) (keepAliveTimeMillis ^ (keepAliveTimeMillis >>> 32)); + result = 31 * result + (threadFactory != null ? threadFactory.hashCode() : 0); + result = 31 * result + priority; + result = 31 * result + (isDaemon ? 1 : 0); + result = 31 * result + (initialClassLoader != null ? initialClassLoader.hashCode() : 0); + return result; + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/TransferEncodingParser.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/TransferEncodingParser.java new file mode 100644 index 0000000..af7b482 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/TransferEncodingParser.java
@@ -0,0 +1,296 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.nio.ByteBuffer; + +import static org.glassfish.jersey.jdk.connector.internal.HttpParserUtils.skipSpaces; +import static org.glassfish.jersey.jdk.connector.internal.HttpParserUtils.isSpaceOrTab; + + +/** + * @author Alexey Stashok + * @author Petr Janouch (petr.janouch at oracle.com) + */ +abstract class TransferEncodingParser { + + abstract boolean parse(ByteBuffer input) throws ParseException; + + static TransferEncodingParser createFixedLengthParser(AsynchronousBodyInputStream responseBody, int expectedLength) { + return new FixedLengthEncodingParser(responseBody, expectedLength); + } + + static TransferEncodingParser createChunkParser(AsynchronousBodyInputStream responseBody, + HttpParser httpParser, int maxHeadersSize) { + return new ChunkedEncodingParser(responseBody, httpParser, maxHeadersSize); + } + + private static class FixedLengthEncodingParser extends TransferEncodingParser { + + private final int expectedLength; + private final AsynchronousBodyInputStream responseBody; + private volatile int consumedLength = 0; + + FixedLengthEncodingParser(AsynchronousBodyInputStream responseBody, int expectedLength) { + this.expectedLength = expectedLength; + this.responseBody = responseBody; + } + + @Override + boolean parse(ByteBuffer input) throws ParseException { + if (input.remaining() + consumedLength > expectedLength) { + throw new ParseException(LocalizationMessages.HTTP_BODY_SIZE_OVERFLOW()); + } + + byte[] data = new byte[input.remaining()]; + input.get(data); + ByteBuffer parsed = ByteBuffer.wrap(data); + responseBody.notifyDataAvailable(parsed); + consumedLength += data.length; + + return consumedLength == expectedLength; + } + } + + private static class ChunkedEncodingParser extends TransferEncodingParser { + + private static final int MAX_HTTP_CHUNK_SIZE_LENGTH = 16; + private static final long CHUNK_SIZE_OVERFLOW = Long.MAX_VALUE >> 4; + + private static final int CHUNK_LENGTH_PARSED_STATE = 3; + + private static final int[] DEC = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + private final HttpParserUtils.ContentParsingState contentParsingState = new HttpParserUtils + .ContentParsingState(); + private final HttpParserUtils.HeaderParsingState headerParsingState; + private final AsynchronousBodyInputStream responseBody; + private final HttpParser httpParser; + private final int maxHeadersSize; + + ChunkedEncodingParser(AsynchronousBodyInputStream responseBody, HttpParser httpParser, int maxHeadersSize) { + this.responseBody = responseBody; + this.httpParser = httpParser; + this.headerParsingState = httpParser.getHeaderParsingState(); + this.maxHeadersSize = maxHeadersSize; + } + + // Taken with small modifications from Grizzly ChunkedTransferEncoding.parsePacket + @Override + boolean parse(ByteBuffer input) throws ParseException { + + while (input.hasRemaining()) { + + boolean isLastChunk = contentParsingState.isLastChunk; + // Check if HTTP chunk length was parsed + if (!isLastChunk && contentParsingState.chunkRemainder <= 0) { + if (!parseTrailerCRLF(input)) { + return false; + } + + if (!parseHttpChunkLength(input)) { + // if not a HEAD request and we don't have enough data to + // parse chunk length - shutdownNow execution + return false; + } + } else { + // HTTP content starts from position 0 in the input Buffer (HTTP chunk header is not part of the input Buffer) + //contentParsingState.chunkContentStart = 0; + contentParsingState.chunkContentStart = input.position(); + } + + // Get the position in the input Buffer, where actual HTTP content starts + int chunkContentStart = contentParsingState.chunkContentStart; + + if (contentParsingState.chunkLength == 0) { + // if it's the last HTTP chunk + if (!isLastChunk) { + // set it's the last chunk + contentParsingState.isLastChunk = true; + isLastChunk = true; + // start trailer parsing + initTrailerParsing(); + } + + // Check if trailer is present + if (!parseLastChunkTrailer(input)) { + // if yes - and there is not enough input data - shutdownNow the + // filterchain processing + return false; + } + + // move the content start position after trailer parsing + chunkContentStart = headerParsingState.offset; + } + + if (isLastChunk) { + input.position(chunkContentStart); + return true; + } + + // Get the number of bytes remaining in the current chunk + final long thisPacketRemaining = contentParsingState.chunkRemainder; + // Get the number of content bytes available in the current input Buffer + final int contentAvailable = input.limit() - chunkContentStart; + + input.position(chunkContentStart); + ByteBuffer data; + if (contentAvailable > thisPacketRemaining) { + // If input Buffer has part of the next message - slice it + data = Utils.split(input, (int) (chunkContentStart + thisPacketRemaining)); + + } else { + data = Utils.split(input, chunkContentStart + input.remaining()); + } + + contentParsingState.chunkRemainder -= data.remaining(); + responseBody.notifyDataAvailable(data); + } + + return false; + } + + // Taken with small modifications from Grizzly ChunkedTransferEncoding.parseHttpChunkLength + private boolean parseHttpChunkLength(final ByteBuffer input) throws ParseException { + while (true) { + switch (headerParsingState.state) { + case 0: {// Initialize chunk parsing + final int pos = input.position(); + headerParsingState.start = pos; + headerParsingState.offset = pos; + headerParsingState.packetLimit = pos + MAX_HTTP_CHUNK_SIZE_LENGTH; + headerParsingState.state = 1; + break; + } + + case 1: { // Skip heading spaces (it's not allowed by the spec, but some servers put it there) + final int nonSpaceIdx = skipSpaces(input, + headerParsingState.offset, headerParsingState.packetLimit); + if (nonSpaceIdx == -1) { + headerParsingState.offset = input.limit(); + headerParsingState.state = 1; + + headerParsingState.checkOverflow(LocalizationMessages.HTTP_CHUNK_ENCODING_PREFIX_OVERFLOW()); + return false; + } + + headerParsingState.offset = nonSpaceIdx; + headerParsingState.state = 2; + break; + } + + case 2: { // Scan chunk size + int offset = headerParsingState.offset; + int limit = Math.min(headerParsingState.packetLimit, input.limit()); + long value = headerParsingState.parsingNumericValue; + + while (offset < limit) { + final byte b = input.get(offset); + if (isSpaceOrTab(b) || /*trailing spaces are not allowed by the spec, but some server put it there*/ + b == HttpParserUtils.CR || b == HttpParserUtils.SEMI_COLON) { + headerParsingState.checkpoint = offset; + } else if (b == HttpParserUtils.LF) { + contentParsingState.chunkContentStart = offset + 1; + contentParsingState.chunkLength = value; + contentParsingState.chunkRemainder = value; + + headerParsingState.state = CHUNK_LENGTH_PARSED_STATE; + + return true; + } else if (headerParsingState.checkpoint == -1) { + if (DEC[b & 0xFF] != -1 && checkOverflow(value)) { + value = (value << 4) + (DEC[b & 0xFF]); + } else { + throw new ParseException( + LocalizationMessages.HTTP_INVALID_CHUNK_SIZE_HEX_VALUE(b)); + } + } else { + throw new ParseException(LocalizationMessages.HTTP_UNEXPECTED_CHUNK_HEADER()); + } + + offset++; + } + + headerParsingState.parsingNumericValue = value; + headerParsingState.offset = offset; + headerParsingState.checkOverflow(LocalizationMessages.HTTP_CHUNK_ENCODING_PREFIX_OVERFLOW()); + return false; + } + } + } + } + + // Taken with small modifications from Grizzly ChunkedTransferEncoding.parseTrailerCRLF + private boolean parseTrailerCRLF(ByteBuffer input) { + if (headerParsingState.state == CHUNK_LENGTH_PARSED_STATE) { + while (input.hasRemaining()) { + if (input.get() == HttpParserUtils.LF) { + headerParsingState.recycle(); + return input.hasRemaining(); + } + } + + return false; + } + + return true; + } + + /** + * @return <tt>false</tt> if next left bit-shift by 4 bits will cause overflow, + * or <tt>true</tt> otherwise + */ + private boolean checkOverflow(final long chunkLength) { + return chunkLength <= CHUNK_SIZE_OVERFLOW; + } + + private void initTrailerParsing() { + headerParsingState.subState = 0; + final int start = contentParsingState.chunkContentStart; + headerParsingState.start = start; + headerParsingState.offset = start; + headerParsingState.packetLimit = start + maxHeadersSize; + } + + // Taken with small modifications from Grizzly ChunkedTransferEncoding.parseLastChunkTrailer + private boolean parseLastChunkTrailer(final ByteBuffer input) throws ParseException { + boolean result = httpParser.parseHeadersFromBuffer(input, true); + if (!result) { + headerParsingState.checkOverflow(LocalizationMessages.HTTP_TRAILER_HEADER_OVERFLOW()); + } + + return result; + } + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/TransportFilter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/TransportFilter.java new file mode 100644 index 0000000..3087a55 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/TransportFilter.java
@@ -0,0 +1,486 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Queue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Writes and reads data to and from a socket. Only one {@link #write(ByteBuffer, + * org.glassfish.jersey.jdk.connector.internal.CompletionHandler)} + * method call can be processed at a time. Only one {@link #_read(ByteBuffer)} operation is supported at a time, + * another one is started only after the previous one has completed. Blocking in {@link #onRead(Object)} + * or {@link #onConnect()} method will result in data not being read from a socket until these methods have completed. + * + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class TransportFilter extends Filter<ByteBuffer, ByteBuffer, Void, ByteBuffer> { + + private static final Logger LOGGER = Logger.getLogger(TransportFilter.class.getName()); + private static final AtomicInteger openedConnections = new AtomicInteger(0); + private static final ScheduledExecutorService connectionCloseScheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("jdk-connector-container-idle-timeout"); + thread.setDaemon(true); + return thread; + }); + + private static volatile AsynchronousChannelGroup channelGroup; + private static volatile ScheduledFuture<?> closeWaitTask; + + /** + * {@link ThreadPoolConfig} current {@link #channelGroup} has been created with. + */ + private static volatile ThreadPoolConfig currentThreadPoolConfig; + /** + * Idle timeout that will be used when closing current {@link #channelGroup} + */ + private static volatile Integer currentContainerIdleTimeout; + + private final int inputBufferSize; + private final ThreadPoolConfig threadPoolConfig; + private final int containerIdleTimeout; + + private volatile AsynchronousSocketChannel socketChannel; + + /** + * Constructor. + * <p/> + * If the channel group is not active (all connections have been closed and the shutdown timeout is running) and a new + * transport is created with tread pool configuration different from the one of the current thread pool, the current + * thread pool will be shut down and a new one created with the new configuration. + * + * @param inputBufferSize size of buffer to be allocated for reading data from a socket. + * @param threadPoolConfig thread pool configuration used for creating thread pool. + * @param containerIdleTimeout idle time after which the shared thread pool will be destroyed. + */ + TransportFilter(int inputBufferSize, ThreadPoolConfig threadPoolConfig, int containerIdleTimeout) { + super(null); + this.inputBufferSize = inputBufferSize; + this.threadPoolConfig = threadPoolConfig; + this.containerIdleTimeout = containerIdleTimeout; + } + + @Override + void write(ByteBuffer data, + final org.glassfish.jersey.jdk.connector.internal.CompletionHandler<ByteBuffer> completionHandler) { + socketChannel.isOpen(); + socketChannel.write(data, data, new CompletionHandler<Integer, ByteBuffer>() { + + @Override + public void completed(Integer result, ByteBuffer buffer) { + if (buffer.hasRemaining()) { + write(buffer, completionHandler); + return; + } + completionHandler.completed(buffer); + } + + @Override + public void failed(Throwable exc, ByteBuffer buffer) { + completionHandler.failed(exc); + } + }); + } + + @Override + void close() { + if (socketChannel == null || !socketChannel.isOpen()) { + return; + } + try { + socketChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.INFO, LocalizationMessages.TRANSPORT_CONNECTION_NOT_CLOSED(), e); + } + synchronized (TransportFilter.class) { + openedConnections.decrementAndGet(); + if (openedConnections.get() == 0 && channelGroup != null) { + scheduleClose(); + } + } + } + + @Override + void startSsl() { + onSslHandshakeCompleted(); + } + + @Override + public void handleConnect(SocketAddress serverAddress, Filter upstreamFilter) { + this.upstreamFilter = upstreamFilter; + + try { + synchronized (TransportFilter.class) { + updateThreadPoolConfig(); + initializeChannelGroup(); + socketChannel = AsynchronousSocketChannel.open(channelGroup); + openedConnections.incrementAndGet(); + } + } catch (IOException e) { + onError(e); + return; + } + + socketChannel.connect(serverAddress, null, new CompletionHandler<Void, Void>() { + + @Override + public void completed(Void result, Void nothing) { + final ByteBuffer inputBuffer = ByteBuffer.allocate(inputBufferSize); + onConnect(); + _read(inputBuffer); + } + + @Override + public void failed(Throwable exc, Void nothing) { + onError(exc); + + try { + socketChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.FINE, LocalizationMessages.TRANSPORT_CONNECTION_NOT_CLOSED(), exc.getMessage()); + } + } + }); + } + + private void updateThreadPoolConfig() { + + // the channel group is active, no change in configuration + if (openedConnections.get() != 0) { + return; + } + + // check if the new configuration is different from the one of the current container + if (!threadPoolConfig.equals(currentThreadPoolConfig) || containerIdleTimeout != currentContainerIdleTimeout) { + + currentThreadPoolConfig = threadPoolConfig; + currentContainerIdleTimeout = containerIdleTimeout; + + if (channelGroup == null) { + // the channel group has not been initialized (this is a first client) - no need to shut it down + return; + } + + closeWaitTask.cancel(true); + closeWaitTask = null; + channelGroup.shutdown(); + channelGroup = null; + } + } + + private void initializeChannelGroup() throws IOException { + if (closeWaitTask != null) { + closeWaitTask.cancel(true); + closeWaitTask = null; + } + + if (channelGroup == null) { + ThreadFactory threadFactory = threadPoolConfig.getThreadFactory(); + if (threadFactory == null) { + threadFactory = new TransportThreadFactory(threadPoolConfig); + } + + ExecutorService executor; + if (threadPoolConfig.getQueue() != null) { + executor = new QueuingExecutor(threadPoolConfig.getCorePoolSize(), threadPoolConfig.getMaxPoolSize(), + threadPoolConfig.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, + threadPoolConfig.getQueue(), false, threadFactory); + } else { + int taskQueueLimit = threadPoolConfig.getQueueLimit(); + if (taskQueueLimit == -1) { + taskQueueLimit = Integer.MAX_VALUE; + } + + executor = new QueuingExecutor(threadPoolConfig.getCorePoolSize(), threadPoolConfig.getMaxPoolSize(), + threadPoolConfig.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, + new LinkedBlockingDeque<>(taskQueueLimit), true, threadFactory); + } + + // Thread pool is owned by the channel group and will be shut down when channel group is shut down + channelGroup = AsynchronousChannelGroup.withCachedThreadPool(executor, threadPoolConfig.getCorePoolSize()); + } + } + + private void _read(final ByteBuffer inputBuffer) { + /** + * It must be checked that the channel has not been closed by {@link #close()} method. + */ + if (!socketChannel.isOpen()) { + return; + } + + socketChannel.read(inputBuffer, null, new CompletionHandler<Integer, Void>() { + @Override + public void completed(Integer bytesRead, Void result) { + // connection closed by the server + if (bytesRead == -1) { + // close will set TransportFilter.this.upstreamFilter to null + Filter upstreamFilter = TransportFilter.this.upstreamFilter; + close(); + upstreamFilter.onConnectionClosed(); + return; + } + + inputBuffer.flip(); + onRead(inputBuffer); + inputBuffer.compact(); + _read(inputBuffer); + } + + @Override + public void failed(Throwable exc, Void result) { + /** + * Reading from the channel will fail if it is closing. In such cases {@link java.nio.channels + * .AsynchronousCloseException} + * is thrown. This should not be logged and no action undertaken. + */ + if (exc instanceof AsynchronousCloseException) { + return; + } + + onError(exc); + } + }); + } + + private void scheduleClose() { + closeWaitTask = connectionCloseScheduler.schedule(() -> { + synchronized (TransportFilter.class) { + if (closeWaitTask == null) { + return; + } + channelGroup.shutdown(); + channelGroup = null; + closeWaitTask = null; + } + }, currentContainerIdleTimeout, TimeUnit.MILLISECONDS); + } + + /** + * A default thread factory that gets used if {@link ThreadPoolConfig#getThreadFactory()} + * is not specified. + */ + private static class TransportThreadFactory implements ThreadFactory { + + private static final String THREAD_NAME_BASE = "jdk-connector-"; + private static final AtomicInteger threadCounter = new AtomicInteger(0); + + private final ThreadPoolConfig threadPoolConfig; + + TransportThreadFactory(ThreadPoolConfig threadPoolConfig) { + this.threadPoolConfig = threadPoolConfig; + } + + @Override + public Thread newThread(Runnable r) { + final Thread thread = new Thread(r); + thread.setName(THREAD_NAME_BASE + threadCounter.incrementAndGet()); + thread.setPriority(threadPoolConfig.getPriority()); + thread.setDaemon(threadPoolConfig.isDaemon()); + + try { + AccessController.doPrivileged(new PrivilegedAction<Void>() { + @Override + public Void run() { + if (threadPoolConfig.getInitialClassLoader() == null) { + thread.setContextClassLoader(this.getClass().getClassLoader()); + } else { + thread.setContextClassLoader(threadPoolConfig.getInitialClassLoader()); + } + return null; + } + }); + } catch (Throwable t) { + // just log - client still can work without setting context class loader + LOGGER.log(Level.WARNING, LocalizationMessages.TRANSPORT_SET_CLASS_LOADER_FAILED(), t); + } + + return thread; + } + } + + /** + * A thread pool executor that prefers creating new worker threads over queueing tasks until the maximum poll size + * has been reached, after which it will start queueing tasks. + */ + private static class QueuingExecutor extends ThreadPoolExecutor { + + private final Queue<Runnable> taskQueue; + private final boolean threadSafeQueue; + + /** + * Constructor. + * + * @param threadSafeQueue indicates if {@link #taskQueue} is thread safe or not. + */ + QueuingExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + Queue<Runnable> taskQueue, + boolean threadSafeQueue, + ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, new HandOffQueue(taskQueue, threadSafeQueue), + threadFactory); + this.taskQueue = taskQueue; + this.threadSafeQueue = threadSafeQueue; + } + + /** + * Submit a task for execution, if the maximum thread limit has been reached and all the threads are occupied, + * enqueue the task. The task is not executed by the current thread, but by a thread from the thread pool. + * + * @param task to be executed. + */ + @Override + public void execute(Runnable task) { + try { + super.execute(task); + } catch (RejectedExecutionException e) { + + /* execution has been rejected either because the executor has been shut down or all worker threads are + * busy - check the former one + */ + if (isShutdown()) { + throw new RejectedExecutionException(LocalizationMessages.TRANSPORT_EXECUTOR_CLOSED(), e); + } + + /* All threads are occupied, try enqueuing the task. + * Each worker thread checks the queue after it has finished executing its task. + */ + if (threadSafeQueue) { + if (!taskQueue.offer(task)) { + throw new RejectedExecutionException(LocalizationMessages.TRANSPORT_EXECUTOR_QUEUE_LIMIT_REACHED(), e); + } + } else { + synchronized (taskQueue) { + if (!taskQueue.offer(task)) { + throw new RejectedExecutionException(LocalizationMessages.TRANSPORT_EXECUTOR_QUEUE_LIMIT_REACHED(), + e); + } + } + } + + /** + * There is a small time interval between a worker thread checks {@link #taskQueue} and it starts to block + * waiting for a new tasks to be submitted (Ideally checking that the {@link #taskQueue} is empty and starting to + * block at the task hand off queue would be atomic). This can be detected by the situation when a thread + * submitting + * a new tasks has been rejected, but not all worker threads are active (However this does not indicate + * exclusively + * the problematic situation). + */ + if (getActiveCount() < getMaximumPoolSize()) { + /* + * There is no guarantee that the same tasks that has been enqueued above will be dequeued, + * but trying to execute one arbitrary task by everyone in this situation is enough to clear the queue. + */ + Runnable dequeuedTask; + if (threadSafeQueue) { + dequeuedTask = taskQueue.poll(); + } else { + synchronized (taskQueue) { + dequeuedTask = taskQueue.poll(); + } + } + + // check if the task has not been consumed by a worker thread after all + if (dequeuedTask != null) { + execute(dequeuedTask); + } + } + } + } + + /** + * Synchronous queue that tries to empty {@link #taskQueue} before it blocks waiting for new tasks to be submitted. + * It is passed to {@link ThreadPoolExecutor}, where it is used used to hand off tasks from task-submitting + * thread to worker threads. + */ + private static class HandOffQueue extends SynchronousQueue<Runnable> { + + private static final long serialVersionUID = -1607064661828834847L; + private final Queue<Runnable> taskQueue; + private final boolean threadSafeQueue; + + private HandOffQueue(Queue<Runnable> taskQueue, boolean threadSafeQueue) { + this.taskQueue = taskQueue; + this.threadSafeQueue = threadSafeQueue; + } + + @Override + public Runnable take() throws InterruptedException { + // try to empty the task queue + Runnable task; + if (threadSafeQueue) { + task = taskQueue.poll(); + } else { + synchronized (taskQueue) { + task = taskQueue.poll(); + } + } + if (task != null) { + return task; + } + + // block and wait for a task to be submitted + return super.take(); + } + + @Override + public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { + // try to empty the task queue + Runnable task; + if (threadSafeQueue) { + task = taskQueue.poll(); + } else { + synchronized (taskQueue) { + task = taskQueue.poll(); + } + } + if (task != null) { + return task; + } + + // block and wait for a task to be submitted + return super.poll(timeout, unit); + } + } + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Utils.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Utils.java new file mode 100644 index 0000000..994842e --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/Utils.java
@@ -0,0 +1,94 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.URI; +import java.nio.ByteBuffer; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class Utils { + + /** + * Concatenates two buffers into one. If buffer given as first argument has enough space for putting + * the other one, it will be done and the original buffer will be returned. Otherwise new buffer will + * be created. + * + * @param buffer first buffer. + * @param buffer1 second buffer. + * @return concatenation. + */ + static ByteBuffer appendBuffers(ByteBuffer buffer, ByteBuffer buffer1, int incomingBufferSize, int BUFFER_STEP_SIZE) { + + final int limit = buffer.limit(); + final int capacity = buffer.capacity(); + final int remaining = buffer.remaining(); + final int len = buffer1.remaining(); + + // buffer1 will be appended to buffer + if (len < (capacity - limit)) { + + buffer.mark(); + buffer.position(limit); + buffer.limit(capacity); + buffer.put(buffer1); + buffer.limit(limit + len); + buffer.reset(); + return buffer; + // Remaining data is moved to left. Then new data is appended + } else if (remaining + len < capacity) { + buffer.compact(); + buffer.put(buffer1); + buffer.flip(); + return buffer; + // create new buffer + } else { + int newSize = remaining + len; + if (newSize > incomingBufferSize) { + throw new IllegalArgumentException("Buffer overflow"); + } else { + final int roundedSize = + (newSize % BUFFER_STEP_SIZE) > 0 ? ((newSize / BUFFER_STEP_SIZE) + 1) * BUFFER_STEP_SIZE : newSize; + final ByteBuffer result = ByteBuffer.allocate(roundedSize > incomingBufferSize ? newSize : roundedSize); + result.put(buffer); + result.put(buffer1); + result.flip(); + return result; + } + } + } + + static ByteBuffer split(ByteBuffer buffer, int position) { + int bytesLength = position - buffer.position(); + byte[] bytes = new byte[bytesLength]; + buffer.get(bytes); + return ByteBuffer.wrap(bytes); + } + + static int getPort(URI uri) { + if (uri.getPort() != -1) { + return uri.getPort(); + } + + if ("https".equals(uri.getScheme())) { + return 443; + } + + return 80; + } +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/WriteListener.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/WriteListener.java new file mode 100644 index 0000000..f9ddd6f --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/WriteListener.java
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; + +/** + * Callback notification mechanism that signals to the developer it's possible + * to write content without blocking. + * <p/> + * Based on Servlet 3.1 + */ +interface WriteListener { + + /** + * When an instance of the WriteListener is registered with a {@link BodyOutputStream}, + * this method will be invoked by the container the first time when it is possible + * to write data. Subsequently the container will invoke this method if and only + * if {@link BodyOutputStream#isReady()} method + * has been called and has returned <code>false</code>. + * + * @throws IOException if an I/O related error has occurred during processing + */ + void onWritePossible() throws IOException; + + /** + * Invoked when an error occurs writing data using the non-blocking APIs. + */ + void onError(final Throwable t); +}
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/package-info.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/package-info.java new file mode 100644 index 0000000..ce249f7 --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Jdk {@link org.glassfish.jersey.client.spi.Connector connector} internal classes. + */ +package org.glassfish.jersey.jdk.connector.internal;
diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/package-info.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/package-info.java new file mode 100644 index 0000000..f70b05c --- /dev/null +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on JDK NIO framework. + */ +package org.glassfish.jersey.jdk.connector;
diff --git a/connectors/jdk-connector/src/main/resources/org/glassfish/jersey/jdk/connector/internal/localization.properties b/connectors/jdk-connector/src/main/resources/org/glassfish/jersey/jdk/connector/internal/localization.properties new file mode 100644 index 0000000..e8f0342 --- /dev/null +++ b/connectors/jdk-connector/src/main/resources/org/glassfish/jersey/jdk/connector/internal/localization.properties
@@ -0,0 +1,73 @@ +# +# Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +read.listener.set.only.once="Read listener can be set only once." +async.operation.not.supported="Operation not supported in synchronous mode." +sync.operation.not.supported="Operation not supported in asynchronous mode." +write.when.not.ready="Asynchronous write called when stream is in non-ready state." +stream.closed.for.input="This stream has already been closed for input." +write.listener.set.only.once="Write listener can be set only once." +stream.closed="The stream has been closed." +writing.failed="Writing data failed" +buffer.incorrect.length="Buffer passed for encoding is neither a multiple of chunkSize nor smaller than chunkSize." +connector.configuration="Connector configuration: {0}." +negative.chunk.size="Configured chunk size is negative: {0}, using default value: {1}." +timeout.receiving.response="Timeout receiving response." +timeout.receiving.response.body="Timeout receiving response body." +closed.while.sending.request="Connection closed by the server while sending request". +closed.while.receiving.response="Connection closed by the server while receiving response." +closed.while.receiving.body="Connection closed by the server while receiving response body." +connection.closed="Connection closed by the server." +closed.by.client.while.sending="Connection closed by the client while sending request." +closed.by.client.while.receiving="Connection closed by the client while receiving response." +closed.by.client.while.receiving.body="Connection closed by the client while receiving response body." +connection.timeout="Connection timed out." +connection.changing.state="HTTP connection {0}:{1} changing state {2} -> {3}." +unexpected.data.in.buffer="Unexpected data remain in the buffer after the HTTP response has been parsed." +http.initial.line.overflow="HTTP packet initial line is too large." +http.packet.header.overflow="HTTP packet header is too large." +http.negative.content.length="Content length cannot be less than 0." +http.invalid.content.length="Invalid format of content length code." +http.request.no.body="This HTTP request does not have a body." +http.request.no.buffered.body="Buffered body is available only in buffered body mode." +http.request.body.size.not.available="Body size is not available in chunked body mode." +proxy.user.name.missing="User name is missing" +proxy.password.missing="Password is missing" +proxy.qop.no.supported="The 'qop' (quality of protection) = {0} extension requested by the server is not supported. Cannot authenticate against the server using Http Digest Authentication." +proxy.407.twice="Received 407 for the second time." +proxy.fail.auth.header="Creating authorization header failed." +proxy.connect.fail="Connecting to proxy failed with status {0}." +proxy.missing.auth.header="Proxy-Authenticate header value is missing or empty." +proxy.unsupported.scheme="Unsupported scheme: {0}." +redirect.no.location="Received redirect that does not contain a location or the location is empty." +redirect.error.determining.location="Error determining redirect location." +redirect.infinite.loop="Infinite loop in chained redirects detected." +redirect.limit.reached="Max chained redirect limit ({0}) exceeded." +ssl.session.closed="SSL session has been closed." +http.body.size.overflow="Body size exceeds declared size" +http.invalid.chunk.size.hex.value="Invalid byte representing a hex value within a chunk length encountered : {0}" +http.unexpected.chunk.header="Unexpected HTTP chunk header." +http.chunk.encoding.prefix.overflow="The chunked encoding length prefix is too large." +http.trailer.header.overflow="The chunked encoding trailer header is too large." +transport.connection.not.closed="Could not close a connection." +transport.set.class.loader.failed="Cannot set thread context class loader." +transport.executor.closed="Cannot set thread context class loader." +transport.executor.queue.limit.reached="A limit of client thread pool queue has been reached." +thread.pool.max.size.too.small="Max thread pool size cannot be smaller than 3." +thread.pool.core.size.too.small="Core thread pool size cannot be smaller than 0." +http.connection.establishing.illegal.state="Cannot try to establish connection if the connection is in other than CREATED state\ + . Current state: {0}. +http.connection.not.idle="Http request cannot be sent over a connection that is in other state than IDLE. Current state: {0}"
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyInputStreamTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyInputStreamTest.java new file mode 100644 index 0000000..249f645 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyInputStreamTest.java
@@ -0,0 +1,402 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static junit.framework.TestCase.assertNull; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class AsynchronousBodyInputStreamTest { + + @Test + public void testBasicAsyncRead() { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + doTestBasicAsyncRead(stream, new TestReadListener(stream)); + } + + @Test + public void testBasicAsyncArrayRead() { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + doTestBasicAsyncRead(stream, new TestReadListener(stream, 15)); + } + + @Test + public void testBasicAsyncReadWithException() { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + doTestBasicAsyncReadWithException(stream, new TestReadListener(stream)); + } + + @Test + public void testBasicAsyncArrayReadWithException() { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + doTestBasicAsyncReadWithException(stream, new TestReadListener(stream, 15)); + } + + @Test + public void testListenerExecutor() throws InterruptedException { + final AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Thread mainThread = Thread.currentThread(); + final AtomicReference<Thread> dataAvailableThread = new AtomicReference<>(); + final AtomicReference<Thread> allDataReadThread = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + try { + stream.setListenerExecutor(executor); + stream.setReadListener(new ReadListener() { + @Override + public void onDataAvailable() throws IOException { + dataAvailableThread.set(Thread.currentThread()); + while (stream.isReady()) { + stream.read(); + + } + } + + @Override + public void onAllDataRead() { + allDataReadThread.set(Thread.currentThread()); + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + + } + }); + + stream.notifyDataAvailable(stringToBuffer("Message")); + stream.notifyAllDataRead(); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + } + + assertNotEquals(mainThread, dataAvailableThread.get()); + assertNotEquals(mainThread, allDataReadThread.get()); + } + + @Test + public void testDataBeforeAsyncModeCommit() { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + String msg4 = "DDDDD"; + + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + + TestReadListener readListener = new TestReadListener(stream); + stream.setReadListener(readListener); + + assertEquals(msg1 + msg2, readListener.getReceivedData()); + + stream.notifyDataAvailable(stringToBuffer(msg3)); + stream.notifyDataAvailable(stringToBuffer(msg4)); + + stream.notifyAllDataRead(); + + assertEquals(msg1 + msg2 + msg3 + msg4, readListener.getReceivedData()); + assertTrue(readListener.isAllDataRead()); + assertNull(readListener.getError()); + } + + @Test + public void testDataBeforeSyncModeCommit() throws IOException { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + String msg4 = "DDDDD"; + + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + + assertEquals((msg1 + msg2).length(), stream.available()); + + stream.notifyDataAvailable(stringToBuffer(msg3)); + stream.notifyDataAvailable(stringToBuffer(msg4)); + + assertEquals((msg1 + msg2 + msg3 + msg4).length(), stream.available()); + } + + @Test + public void testAllDataBeforeAsyncModeCommit() { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + + stream.notifyAllDataRead(); + + TestReadListener readListener = new TestReadListener(stream); + stream.setReadListener(readListener); + + assertEquals(msg1 + msg2, readListener.getReceivedData()); + assertTrue(readListener.isAllDataRead()); + assertNull(readListener.getError()); + } + + @Test + public void testAllDataBeforeSyncModeCommit() throws IOException { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + + stream.notifyAllDataRead(); + + assertEquals((msg1 + msg2).length(), stream.available()); + } + + @Test + public void testErrorBeforeAsyncModeCommit() throws IOException { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + + Throwable t = new Throwable(); + stream.notifyError(t); + + TestReadListener readListener = new TestReadListener(stream); + stream.setReadListener(readListener); + + assertEquals(msg1 + msg2, readListener.getReceivedData()); + assertFalse(readListener.isAllDataRead()); + assertTrue(readListener.getError() == t); + } + + @Test + public void testErrorBeforeSyncModeCommit() throws IOException { + AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + + Throwable t = new Throwable(); + stream.notifyError(t); + + try { + stream.available(); + fail(); + } catch (Throwable e) { + assertTrue(e.getCause() == t); + } + } + + @Test + public void testUnsupportedSync() { + final AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + try { + // touch this stream to make it synchronous + stream.tryRead(); + } catch (IOException e) { + e.printStackTrace(); + fail(); + } + + assertUnsupported(() -> { + stream.isReady(); + return null; + }); + + assertUnsupported(() -> { + stream.setReadListener(new TestReadListener(stream)); + return null; + }); + + assertUnsupported(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + stream.setListenerExecutor(executorService); + } finally { + executorService.shutdownNow(); + } + return null; + }); + } + + @Test + public void testUnsupportedAsync() { + final AsynchronousBodyInputStream stream = new AsynchronousBodyInputStream(); + stream.setReadListener(new TestReadListener(stream)); + + assertUnsupported(() -> { + stream.tryRead(); + return null; + }); + + assertUnsupported(() -> { + stream.tryRead(new byte[10]); + return null; + }); + + assertUnsupported(() -> { + stream.tryRead(new byte[10], 0, 10); + return null; + }); + + assertUnsupported(() -> stream.skip(10)); + + assertUnsupported(() -> stream.available()); + } + + private void doTestBasicAsyncRead(AsynchronousBodyInputStream stream, TestReadListener readListener) { + stream.setReadListener(readListener); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + stream.notifyDataAvailable(stringToBuffer(msg3)); + stream.notifyAllDataRead(); + + assertEquals(msg1 + msg2 + msg3, readListener.getReceivedData()); + assertTrue(readListener.isAllDataRead()); + assertNull(readListener.getError()); + } + + private void doTestBasicAsyncReadWithException(AsynchronousBodyInputStream stream, TestReadListener readListener) { + stream.setReadListener(readListener); + + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + stream.notifyDataAvailable(stringToBuffer(msg1)); + stream.notifyDataAvailable(stringToBuffer(msg2)); + stream.notifyDataAvailable(stringToBuffer(msg3)); + Throwable t = new Throwable(); + stream.notifyError(t); + + assertEquals(msg1 + msg2 + msg3, readListener.getReceivedData()); + assertFalse(readListener.isAllDataRead()); + assertTrue(readListener.getError() == t); + } + + private static void assertUnsupported(Callable unsupported) { + try { + unsupported.call(); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + private static ByteBuffer stringToBuffer(String str) { + return ByteBuffer.wrap(str.getBytes()); + } + + private static class TestReadListener implements ReadListener { + + private final ByteArrayOutputStream receivedData = new ByteArrayOutputStream(); + private final AsynchronousBodyInputStream inputStream; + private final int inputArraySize; + + private volatile Throwable error = null; + private volatile boolean allDataRead = false; + private volatile boolean listenerCallExpected = true; + + public TestReadListener(AsynchronousBodyInputStream inputStream, int inputArraySize) { + this.inputStream = inputStream; + this.inputArraySize = inputArraySize; + } + + public TestReadListener(AsynchronousBodyInputStream inputStream) { + this.inputStream = inputStream; + this.inputArraySize = -1; + } + + @Override + public void onDataAvailable() throws IOException { + if (!listenerCallExpected) { + fail(); + } + + listenerCallExpected = false; + while (inputStream.isReady()) { + if (inputArraySize == -1) { + receivedData.write(inputStream.read()); + } else { + byte[] inputArray = new byte[inputArraySize]; + int read = inputStream.read(inputArray); + receivedData.write(inputArray, 0, read); + } + } + + listenerCallExpected = true; + } + + @Override + public void onAllDataRead() { + allDataRead = true; + } + + @Override + public void onError(Throwable t) { + error = t; + } + + public boolean isAllDataRead() { + return allDataRead; + } + + public Throwable getError() { + return error; + } + + public String getReceivedData() { + return new String(receivedData.toByteArray()); + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyOutputStreamTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyOutputStreamTest.java new file mode 100644 index 0000000..09e21be --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/AsynchronousBodyOutputStreamTest.java
@@ -0,0 +1,606 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritePendingException; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static junit.framework.Assert.assertNotNull; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class AsynchronousBodyOutputStreamTest { + + @Test + public void testBasicAsyncWrite() throws IOException { + doTestAsyncWrite(false); + } + + @Test + public void testBasicAsyncArrayWrite() throws IOException { + doTestAsyncWrite(true); + } + + @Test + public void testSetListenerAfterOpeningStream() throws IOException { + TestStream stream = new TestStream(6); + MockTransportFilter transportFilter = new MockTransportFilter(); + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + + TestWriteListener writeListener = new TestWriteListener(stream, -1); + writeListener.write(msg1); + stream.open(transportFilter); + stream.setWriteListener(writeListener); + writeListener.write(msg2); + writeListener.write(msg3); + stream.close(); + + if (writeListener.getError() != null) { + writeListener.getError().printStackTrace(); + fail(); + } + assertEquals(msg1 + msg2 + msg3, transportFilter.getWrittenData()); + } + + @Test + public void testTestAsyncWriteWithDelay() throws IOException { + doTestAsyncWriteWithDelay(false); + } + + @Test + public void testTestAsyncWriteArrayWithDelay() throws IOException { + doTestAsyncWriteWithDelay(true); + } + + @Test + public void testAsyncFlush() { + TestStream stream = new TestStream(6); + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + String msg4 = "DDDDDDD"; + TestWriteListener writeListener = new TestWriteListener(stream, -1); + stream.setWriteListener(writeListener); + + MockTransportFilter transportFilter = new MockTransportFilter(); + writeListener.write(msg1); + transportFilter.block(); + stream.open(transportFilter); + writeListener.flush(); + // test someone going crazy with flush + writeListener.flush(); + transportFilter.unblock(); + transportFilter.block(); + writeListener.write(msg2); + transportFilter.unblock(); + writeListener.flush(); + transportFilter.block(); + writeListener.write(msg3); + writeListener.flush(); + writeListener.write(msg4); + + writeListener.flush(); + writeListener.close(); + transportFilter.unblock(); + + if (writeListener.getError() != null) { + writeListener.getError().printStackTrace(); + fail(); + } + + assertEquals(msg1 + msg2 + msg3 + msg4, transportFilter.getWrittenData()); + } + + @Test + public void testAsyncException() { + TestStream stream = new TestStream(6); + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + + TestWriteListener writeListener = new TestWriteListener(stream, -1); + stream.setWriteListener(writeListener); + + MockTransportFilter transportFilter = new MockTransportFilter(); + stream.open(transportFilter); + Throwable t = new Throwable(); + transportFilter.setException(t); + writeListener.write(msg1); + + assertNotNull(writeListener.getError()); + assertTrue(t == writeListener.getError()); + } + + @Test + public void testBasicSyncWrite() throws IOException, InterruptedException, TimeoutException, ExecutionException { + doTestSyncWrite(false); + } + + @Test + public void testBasicSyncArrayWrite() throws IOException, InterruptedException, TimeoutException, ExecutionException { + doTestSyncWrite(true); + } + + @Test + public void testSyncWriteWithDelay() throws IOException, InterruptedException, TimeoutException, ExecutionException { + doTestSyncWriteWithDelay(false); + } + + @Test + public void testSyncArrayWriteWithDelay() throws IOException, InterruptedException, TimeoutException, ExecutionException { + doTestSyncWriteWithDelay(true); + } + + @Test + public void testAsyncWriteWhenNotReady() throws IOException { + TestStream stream = new TestStream(6); + TestWriteListener writeListener = new TestWriteListener(stream, -1); + stream.setWriteListener(writeListener); + + try { + stream.write((byte) 'a'); + fail(); + } catch (IllegalStateException e) { + // expected + } + } + + @Test + public void testUnsupportedSync() { + final TestStream stream = new TestStream(10); + stream.open(new MockTransportFilter()); + try { + // touch this stream to make it synchronous + stream.write((byte) 'a'); + } catch (IOException e) { + e.printStackTrace(); + fail(); + } + + assertUnsupported(() -> { + stream.isReady(); + return null; + }); + + assertUnsupported(() -> { + stream.setWriteListener(new TestWriteListener(stream)); + return null; + }); + } + + @Test + public void testSyncException() throws IOException { + TestStream stream = new TestStream(1); + + MockTransportFilter transportFilter = new MockTransportFilter(); + stream.open(transportFilter); + Throwable t = new Throwable(); + transportFilter.setException(t); + try { + stream.write("aaa".getBytes()); + fail(); + } catch (IOException e) { + assertTrue(t == e.getCause()); + } + } + + private void doTestSyncWrite(final boolean useArray) + throws IOException, InterruptedException, TimeoutException, ExecutionException { + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + try { + final TestStream stream = new TestStream(6); + final String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + + MockTransportFilter transportFilter = new MockTransportFilter(); + Future<Boolean> future = executor.submit(() -> { + try { + writeToStream(stream, msg1, useArray); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + }); + + // test that synchronous write really blocks until the stream is opened + assertFalse(future.isDone()); + stream.open(transportFilter); + + assertTrue(future.get(300, TimeUnit.SECONDS)); + writeToStream(stream, msg2, useArray); + writeToStream(stream, msg3, useArray); + stream.close(); + assertEquals(msg1 + msg2 + msg3, transportFilter.getWrittenData()); + } finally { + executor.shutdownNow(); + } + } + + private void doTestAsyncWriteWithDelay(boolean useArray) throws IOException { + int arraySize = -1; + if (useArray) { + arraySize = 10; + } + + TestStream stream = new TestStream(6); + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + String msg4 = "DDDDDDD"; + TestWriteListener writeListener = new TestWriteListener(stream, arraySize); + stream.setWriteListener(writeListener); + + MockTransportFilter transportFilter = new MockTransportFilter(); + writeListener.write(msg1); + transportFilter.block(); + stream.open(transportFilter); + transportFilter.unblock(); + transportFilter.block(); + writeListener.write(msg2); + transportFilter.unblock(); + transportFilter.block(); + writeListener.write(msg3); + writeListener.write(msg4); + + writeListener.close(); + transportFilter.unblock(); + + if (writeListener.getError() != null) { + writeListener.getError().printStackTrace(); + fail(); + } + + assertEquals(msg1 + msg2 + msg3 + msg4, transportFilter.getWrittenData()); + } + + private void writeToStream(TestStream stream, String msg, boolean useArray) throws IOException { + if (useArray) { + stream.write(msg.getBytes()); + } else { + byte[] bytes = msg.getBytes(); + for (byte b : bytes) { + stream.write(b); + } + } + } + + private void doTestSyncWriteWithDelay(final boolean useArray) + throws IOException, InterruptedException, TimeoutException, ExecutionException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + try { + final TestStream stream = new TestStream(6); + final String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + final String msg2 = "BBBBBBBBBBBBB"; + final String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + final String msg4 = "DDDDDDD"; + + MockTransportFilter transportFilter = new MockTransportFilter(); + stream.open(transportFilter); + + final CountDownLatch blockLatch1 = new CountDownLatch(1); + transportFilter.block(blockLatch1); + + Future<Boolean> future = executor.submit(() -> { + try { + writeToStream(stream, msg1, useArray); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + }); + + assertTrue(blockLatch1.await(5, TimeUnit.SECONDS)); + assertFalse(future.isDone()); + + transportFilter.unblock(); + assertTrue(future.get(5, TimeUnit.SECONDS)); + + final CountDownLatch blockLatch2 = new CountDownLatch(1); + transportFilter.block(blockLatch2); + + future = executor.submit(() -> { + try { + writeToStream(stream, msg2, useArray); + writeToStream(stream, msg3, useArray); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + }); + + assertTrue(blockLatch2.await(5, TimeUnit.SECONDS)); + assertFalse(future.isDone()); + transportFilter.unblock(); + assertTrue(future.get(5, TimeUnit.SECONDS)); + + final CountDownLatch blockLatch3 = new CountDownLatch(1); + transportFilter.block(blockLatch3); + + future = executor.submit(() -> { + try { + writeToStream(stream, msg4, useArray); + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + }); + + assertTrue(blockLatch3.await(5, TimeUnit.SECONDS)); + assertFalse(future.isDone()); + transportFilter.unblock(); + assertTrue(future.get(5, TimeUnit.SECONDS)); + + assertEquals(msg1 + msg2 + msg3 + msg4, transportFilter.getWrittenData()); + } finally { + executor.shutdownNow(); + } + } + + private void doTestAsyncWrite(boolean useArray) throws IOException { + int arraySize = -1; + if (useArray) { + arraySize = 10; + } + + TestStream stream = new TestStream(6); + String msg1 = "AAAAAAAAAAAAAAAAAAAA"; + String msg2 = "BBBBBBBBBBBBB"; + String msg3 = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + TestWriteListener writeListener = new TestWriteListener(stream, arraySize); + stream.setWriteListener(writeListener); + + MockTransportFilter transportFilter = new MockTransportFilter(); + writeListener.write(msg1); + stream.open(transportFilter); + writeListener.write(msg2); + writeListener.write(msg3); + stream.close(); + + if (writeListener.getError() != null) { + writeListener.getError().printStackTrace(); + fail(); + } + assertEquals(msg1 + msg2 + msg3, transportFilter.getWrittenData()); + } + + private static void assertUnsupported(Callable unsupported) { + try { + unsupported.call(); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + private static class TestWriteListener implements WriteListener { + + private static final ByteBuffer CLOSE = ByteBuffer.allocate(0); + private static final ByteBuffer FLUSH = ByteBuffer.allocate(0); + + private final ChunkedBodyOutputStream outputStream; + private final Queue<ByteBuffer> message = new LinkedList<>(); + private final int outputArraySize; + + private volatile boolean listenerCallExpected = true; + private volatile Throwable error; + + TestWriteListener(ChunkedBodyOutputStream outputStream) { + this(outputStream, -1); + } + + TestWriteListener(ChunkedBodyOutputStream outputStream, int outputArraySize) { + this.outputStream = outputStream; + + this.outputArraySize = outputArraySize; + } + + void write(String message) { + byte[] bytes = message.getBytes(); + this.message.add(ByteBuffer.wrap(bytes)); + doWrite(); + } + + void close() { + message.add(CLOSE); + doWrite(); + } + + void flush() { + message.add(FLUSH); + doWrite(); + } + + @Override + public void onWritePossible() { + if (!listenerCallExpected) { + fail(); + } + + listenerCallExpected = false; + doWrite(); + } + + private void doWrite() { + + while (message.peek() != null + && (outputStream.isReady() || message.peek() == CLOSE || message.peek() == FLUSH)) { + try { + + ByteBuffer headBuffer = message.peek(); + + if (headBuffer == CLOSE) { + outputStream.close(); + message.poll(); + continue; + } + + if (headBuffer == FLUSH) { + outputStream.flush(); + message.poll(); + continue; + } + + if (outputArraySize == -1) { + outputStream.write(headBuffer.get()); + } else { + int arraySize = outputArraySize; + if (headBuffer.remaining() < arraySize) { + arraySize = headBuffer.remaining(); + } + + byte[] outputArray = new byte[arraySize]; + headBuffer.get(outputArray); + outputStream.write(outputArray); + } + + if (!headBuffer.hasRemaining()) { + message.poll(); + } + } catch (IOException e) { + error = e; + } + } + + if (!outputStream.isReady()) { + listenerCallExpected = true; + } + } + + @Override + public void onError(Throwable t) { + error = t; + } + + public Throwable getError() { + return error; + } + } + + private static class TestStream extends ChunkedBodyOutputStream { + + TestStream(int bufferSize) { + super(bufferSize); + } + + @Override + protected ByteBuffer encodeToHttp(ByteBuffer byteBuffer) { + return byteBuffer; + } + } + + private static class MockTransportFilter extends Filter<ByteBuffer, Void, Void, Void> { + + private final ByteArrayOutputStream writtenData = new ByteArrayOutputStream(); + + private volatile boolean pendingWrite = false; + private volatile boolean block = false; + private volatile CountDownLatch blockLatch; + private volatile CompletionHandler<ByteBuffer> completionHandler; + private volatile Throwable exception; + + MockTransportFilter() { + super(null); + } + + @Override + void write(ByteBuffer data, CompletionHandler<ByteBuffer> completionHandler) { + if (pendingWrite) { + completionHandler.failed(new WritePendingException()); + } + + pendingWrite = true; + + while (data.hasRemaining()) { + writtenData.write(data.get()); + } + + if (block) { + this.completionHandler = completionHandler; + if (blockLatch != null) { + blockLatch.countDown(); + } + return; + } + + pendingWrite = false; + if (exception == null) { + completionHandler.completed(data); + } else { + completionHandler.failed(exception); + } + } + + String getWrittenData() { + return new String(writtenData.toByteArray()); + } + + void block(CountDownLatch blockLatch) { + this.blockLatch = blockLatch; + block = true; + } + + void block() { + block = true; + } + + void unblock() { + block = false; + pendingWrite = false; + completionHandler.completed(null); + completionHandler = null; + } + + public void setException(Throwable exception) { + this.exception = exception; + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ChunkedBodyOutputStreamTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ChunkedBodyOutputStreamTest.java new file mode 100644 index 0000000..d86f05d --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ChunkedBodyOutputStreamTest.java
@@ -0,0 +1,146 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.TestCase.fail; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class ChunkedBodyOutputStreamTest { + + @Test + public void testBasic() throws IOException { + AsynchronousBodyInputStream responseBody = new AsynchronousBodyInputStream(); + ChunkedBodyOutputStream chunkedStream = getOutputStream(responseBody, 21); + + String sentBody = TestUtils.generateBody(500); + byte[] sentBytes = sentBody.getBytes(); + for (byte b : sentBytes) { + chunkedStream.write(b); + } + + chunkedStream.close(); + verifyReceivedMessage(sentBody, responseBody); + } + + @Test + public void testChunkSize() throws IOException { + doTestChunkSize(1); + } + + @Test + public void testChunkSizeWithArray() throws IOException { + doTestChunkSize(8); + } + + private void doTestChunkSize(int batchSize) throws IOException { + final int chunkSize = 21; + AsynchronousBodyInputStream responseBody = new AsynchronousBodyInputStream() { + + private boolean receivedLess = false; + + @Override + synchronized void notifyDataAvailable(ByteBuffer availableData) { + if (availableData.remaining() > chunkSize) { + fail(); + } + + if (availableData.remaining() < chunkSize) { + assertFalse(receivedLess); + receivedLess = true; + } + + super.notifyDataAvailable(availableData); + } + }; + + ChunkedBodyOutputStream chunkedStream = getOutputStream(responseBody, chunkSize); + + String sentBody = TestUtils.generateBody(100); + byte[] sentBytes = sentBody.getBytes(); + if (batchSize > 1) { + for (int i = 0; i < sentBytes.length; i += 8) { + chunkedStream.write(sentBytes, i, Math.min(sentBytes.length - i, 8)); + } + } else { + for (byte b : sentBytes) { + chunkedStream.write(b); + } + } + + chunkedStream.close(); + verifyReceivedMessage(sentBody, responseBody); + } + + private ChunkedBodyOutputStream getOutputStream(AsynchronousBodyInputStream responseBody, int chunkSize) { + ChunkedBodyOutputStream chunkedStream = new ChunkedBodyOutputStream(chunkSize); + Filter<ByteBuffer, ?, ?, ?> mockTransportFilter = createMockTransportFilter(responseBody); + chunkedStream.open(mockTransportFilter); + return chunkedStream; + } + + private void verifyReceivedMessage(String sentBody, AsynchronousBodyInputStream responseBody) throws IOException { + byte[] sentBytes = sentBody.getBytes(); + byte[] receivedBytes = new byte[sentBytes.length]; + + for (int i = 0; i < sentBytes.length; i++) { + int b = responseBody.tryRead(); + if (b == -1) { + fail(); + } + + receivedBytes[i] = (byte) b; + } + + if (responseBody.tryRead() != -1) { + fail(); + } + + String receivedBody = new String(receivedBytes); + assertEquals(sentBody, receivedBody); + } + + Filter<ByteBuffer, ?, ?, ?> createMockTransportFilter(final AsynchronousBodyInputStream responseBody) { + HttpParser parser = new HttpParser(Integer.MAX_VALUE, Integer.MAX_VALUE); + parser.reset(true); + final TransferEncodingParser transferEncodingParser = TransferEncodingParser + .createChunkParser(responseBody, parser, 1000); + return new Filter<ByteBuffer, Void, Void, Void>(null) { + + @Override + public void write(ByteBuffer chunk, CompletionHandler<ByteBuffer> completionHandler) { + try { + if (transferEncodingParser.parse(chunk)) { + responseBody.notifyAllDataRead(); + } + + completionHandler.completed(chunk); + } catch (ParseException e) { + completionHandler.failed(e); + } + } + }; + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ConnectionPoolTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ConnectionPoolTest.java new file mode 100644 index 0000000..ee9d261 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ConnectionPoolTest.java
@@ -0,0 +1,242 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; + +import javax.net.ServerSocketFactory; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class ConnectionPoolTest extends JerseyTest { + + @Test + public void testBasic() throws InterruptedException { + String msg1 = "message 1"; + String msg2 = "message 2"; + CountDownLatch latch = new CountDownLatch(2); + sendMessageToJersey(msg1, latch); + sendMessageToJersey(msg2, latch); + + /* the idle timeout is 10s and only 1 connection is allowed, so the test should fail unless the pool reuses + the connection for both requests */ + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + + private void sendMessageToJersey(String message, final CountDownLatch latch) { + target("echo").request().async().post(Entity.entity(message, MediaType.TEXT_PLAIN), new InvocationCallback<String>() { + @Override + public void completed(String response) { + System.out.println("#Received: " + response); + latch.countDown(); + } + + @Override + public void failed(Throwable throwable) { + throwable.printStackTrace(); + } + }); + } + + @Test + public void testPersistentConnection() throws IOException, InterruptedException { + TestServer testServer = new TestServer(true); + + try { + testServer.start(); + CountDownLatch latch = new CountDownLatch(2); + AtomicInteger result1 = new AtomicInteger(-1); + sendGetToTestServer(result1, latch); + AtomicInteger result2 = new AtomicInteger(-1); + sendGetToTestServer(result2, latch); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + + assertEquals(1, result1.get()); + assertEquals(1, result2.get()); + } finally { + testServer.stop(); + } + } + + @Test + public void testNonPersistentConnection() throws IOException, InterruptedException { + TestServer testServer = new TestServer(false); + + try { + testServer.start(); + CountDownLatch latch1 = new CountDownLatch(1); + AtomicInteger result1 = new AtomicInteger(-1); + sendGetToTestServer(result1, latch1); + assertTrue(latch1.await(5, TimeUnit.SECONDS)); + CountDownLatch latch2 = new CountDownLatch(1); + + AtomicInteger result2 = new AtomicInteger(-1); + sendGetToTestServer(result2, latch2); + + assertTrue(latch2.await(5, TimeUnit.SECONDS)); + + assertEquals(1, result1.get()); + assertEquals(2, result2.get()); + } finally { + testServer.stop(); + } + } + + private void sendGetToTestServer(final AtomicInteger result, final CountDownLatch latch) { + getClient().target("http://localhost:" + TestServer.PORT).request().async().get(new InvocationCallback<Integer>() { + @Override + public void completed(Integer response) { + System.out.println("#Received: " + response); + result.set(response); + latch.countDown(); + } + + @Override + public void failed(Throwable throwable) { + throwable.printStackTrace(); + } + }); + } + + @Override + protected Application configure() { + return new ResourceConfig(EchoResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JdkConnectorProvider()); + config.property(JdkConnectorProperties.MAX_CONNECTIONS_PER_DESTINATION, 1); + config.property(JdkConnectorProperties.CONNECTION_IDLE_TIMEOUT, 10_000); + } + + @Path("/echo") + public static class EchoResource { + + @POST + public String post(String entity) { + return entity; + } + } + + private static class TestServer { + + static final int PORT = 8321; + + private final boolean persistentConnection; + private final ServerSocket serverSocket; + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final AtomicInteger connectionsCount = new AtomicInteger(0); + + private volatile boolean stopped = false; + + TestServer(boolean persistentConnection) throws IOException { + this.persistentConnection = persistentConnection; + ServerSocketFactory socketFactory = ServerSocketFactory.getDefault(); + serverSocket = socketFactory.createServerSocket(PORT); + } + + void start() { + executorService.execute(() -> { + try { + while (!stopped) { + final Socket socket = serverSocket.accept(); + connectionsCount.incrementAndGet(); + executorService.submit(() -> handleConnection(socket)); + + } + } catch (IOException e) { + //do nothing + } + }); + } + + private void handleConnection(Socket socket) { + + try { + InputStream inputStream = socket.getInputStream(); + ByteArrayOutputStream receivedMessage = new ByteArrayOutputStream(); + + while (!stopped && !socket.isClosed()) { + int result = inputStream.read(); + if (result == -1) { + return; + } + + receivedMessage.write((byte) result); + String msg = new String(receivedMessage.toByteArray(), "ASCII"); + if (msg.contains("\r\n\r\n")) { + receivedMessage = new ByteArrayOutputStream(); + OutputStream outputStream = socket.getOutputStream(); + String response = "HTTP/1.1 200 OK\r\nContent-Length: 1\r\nContent-Type: text/plain\r\n"; + if (!persistentConnection) { + response += "Connection: Close\r\n"; + } + response += "\r\n" + connectionsCount.get(); + outputStream.write(response.getBytes("ASCII")); + outputStream.flush(); + } + } + } catch (IOException e) { + if (!e.getClass().equals(SocketException.class)) { + e.printStackTrace(); + } + } finally { + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + void stop() throws IOException { + executorService.shutdown(); + serverSocket.close(); + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/CookieTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/CookieTest.java new file mode 100644 index 0000000..532645d --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/CookieTest.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.CookiePolicy; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Paul Sandoz (paul.sandoz at oracle.com) + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class CookieTest extends JerseyTest { + + @Path("/CookieResource") + public static class CookieResource { + + @GET + public Response get(@Context HttpHeaders h) { + Cookie c = h.getCookies().get("name"); + String e = (c == null) ? "NO-COOKIE" : c.getValue(); + return Response.ok(e).cookie(new NewCookie("name", "value")).build(); + } + } + + @Override + protected Application configure() { + return new ResourceConfig(CookieResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JdkConnectorProvider()); + } + + @Test + public void testCookieResource() { + // the default cookie policy does not like cookies from localhost + WebTarget target = target("CookieResource").property(JdkConnectorProperties.COOKIE_POLICY, CookiePolicy.ACCEPT_ALL); + + assertEquals("NO-COOKIE", target.request().get(String.class)); + assertEquals("value", target.request().get(String.class)); + } + + @Test + public void testDisabledCookies() { + // the default cookie policy does not like cookies from localhost + WebTarget target = target("CookieResource").property(JdkConnectorProperties.COOKIE_POLICY, CookiePolicy.ACCEPT_NONE); + + assertEquals("NO-COOKIE", target.request().get(String.class)); + assertEquals("NO-COOKIE", target.request().get(String.class)); + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/EntityWriteTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/EntityWriteTest.java new file mode 100644 index 0000000..1937aa9 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/EntityWriteTest.java
@@ -0,0 +1,78 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class EntityWriteTest extends JerseyTest { + + private static final String target = "entityWrite"; + + @Override + protected Application configure() { + return new ResourceConfig(EchoResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 20); + config.connectorProvider(new JdkConnectorProvider()); + } + + @Test + public void testBuffered() { + String message = TestUtils.generateBody(5000); + Response response = target(target).request().post(Entity.entity(message, MediaType.TEXT_PLAIN)); + assertEquals(message, response.readEntity(String.class)); + } + + @Test + public void testChunked() { + String message = TestUtils.generateBody(5000); + Response response = target(target).request() + .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED) + .post(Entity.entity(message, MediaType.TEXT_PLAIN)); + assertEquals(message, response.readEntity(String.class)); + } + + @Path("/entityWrite") + public static class EchoResource { + + @POST + public String post(String entity) { + return entity; + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionTest.java new file mode 100644 index 0000000..3a124e2 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionTest.java
@@ -0,0 +1,310 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.net.CookieManager; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.AfterClass; +import org.junit.Test; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.CLOSED; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.CONNECTING; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.CONNECT_TIMEOUT; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.ERROR; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.IDLE; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.IDLE_TIMEOUT; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.RECEIVED; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.RECEIVING_BODY; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.RECEIVING_HEADER; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.RESPONSE_TIMEOUT; +import static org.glassfish.jersey.jdk.connector.internal.HttpConnection.State.SENDING_REQUEST; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class HttpConnectionTest extends JerseyTest { + + private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private static final Throwable testError = new Throwable(); + + @AfterClass + public static void cleanUp() { + scheduler.shutdownNow(); + } + + @Test + public void testBasic() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, IDLE, SENDING_REQUEST, + RECEIVING_HEADER, RECEIVING_BODY, RECEIVED, IDLE}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + doTest(ERROR_STATE.NONE, expectedStates, request); + } + + @Test + public void testMultipleRequests() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, IDLE, SENDING_REQUEST, + RECEIVING_HEADER, RECEIVING_BODY, RECEIVED, IDLE, SENDING_REQUEST, RECEIVING_HEADER, RECEIVING_BODY, RECEIVED, + IDLE}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + doTest(ERROR_STATE.NONE, expectedStates, request, request); + } + + @Test + public void testErrorSending() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, IDLE, SENDING_REQUEST, ERROR, CLOSED}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + doTest(ERROR_STATE.SENDING, expectedStates, request); + } + + @Test + public void testErrorReceiving() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, IDLE, SENDING_REQUEST, + RECEIVING_HEADER, ERROR, CLOSED}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + doTest(ERROR_STATE.RECEIVING_HEADER, expectedStates, request); + } + + @Test + public void testTimeoutConnecting() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, CONNECT_TIMEOUT, CLOSED}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + ConnectorConfiguration configuration = new ConnectorConfiguration(client(), client().getConfiguration()) { + @Override + int getConnectTimeout() { + return 100; + } + }; + doTest(ERROR_STATE.LOST_CONNECT, configuration, expectedStates, request); + } + + @Test + public void testResponseTimeout() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, IDLE, SENDING_REQUEST, + RECEIVING_HEADER, RESPONSE_TIMEOUT, CLOSED}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + ConnectorConfiguration configuration = new ConnectorConfiguration(client(), client().getConfiguration()) { + + @Override + int getResponseTimeout() { + return 100; + } + }; + + doTest(ERROR_STATE.LOST_REQUEST, configuration, expectedStates, request); + } + + @Test + public void testIdleTimeout() { + HttpConnection.State[] expectedStates = new HttpConnection.State[] {CONNECTING, IDLE, SENDING_REQUEST, + RECEIVING_HEADER, RECEIVING_BODY, RECEIVED, IDLE, IDLE_TIMEOUT, CLOSED}; + HttpRequest request = HttpRequest.createBodyless("GET", target("hello").getUri()); + ConnectorConfiguration configuration = new ConnectorConfiguration(client(), client().getConfiguration()) { + + @Override + int getConnectionIdleTimeout() { + return 500; + } + }; + + doTest(ERROR_STATE.NONE, configuration, expectedStates, request); + } + + private void doTest(ERROR_STATE errorState, + ConnectorConfiguration configuration, + HttpConnection.State[] expectedStates, + HttpRequest... httpRequests) { + CountDownLatch latch = new CountDownLatch(1); + TestStateListener stateListener = new TestStateListener(expectedStates, latch, httpRequests); + HttpConnection connection = createConnection(httpRequests[0].getUri(), stateListener, errorState, configuration); + connection.connect(); + + try { + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } catch (Throwable t) { + // continue + } + + assertEquals(Arrays.asList(expectedStates), stateListener.getObservedStates()); + + if (errorState == ERROR_STATE.SENDING || errorState == ERROR_STATE.CONNECTING + || errorState == ERROR_STATE.RECEIVING_HEADER) { + assertTrue(testError == connection.getError()); + } + } + + private void doTest(ERROR_STATE errorState, HttpConnection.State[] expectedStates, HttpRequest... httpRequests) { + ConnectorConfiguration configuration = new ConnectorConfiguration(client(), client().getConfiguration()); + doTest(errorState, configuration, expectedStates, httpRequests); + } + + @Override + protected Application configure() { + return new ResourceConfig(EchoResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(JdkConnectorProperties.CONNECTION_IDLE_TIMEOUT, 30_000); + config.connectorProvider(new JdkConnectorProvider()); + } + + @Path("/hello") + public static class EchoResource { + + @GET + public String getHello() { + return "Hello"; + } + } + + private HttpConnection createConnection(URI uri, + TestStateListener stateListener, + final ERROR_STATE errorState, + ConnectorConfiguration configuration) { + return new HttpConnection(uri, new CookieManager(), configuration, scheduler, stateListener) { + @Override + protected Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> createFilterChain(URI uri, + ConnectorConfiguration + configuration) { + Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> filterChain = super + .createFilterChain(uri, configuration); + return new InterceptorFilter(filterChain, errorState); + } + }; + } + + private static class TestStateListener implements HttpConnection.StateChangeListener { + + private final List<HttpConnection.State> observedStates = new ArrayList<>(); + private final HttpRequest[] httpRequests; + private final AtomicInteger sentRequests = new AtomicInteger(0); + private final CountDownLatch latch; + private final Queue<HttpConnection.State> expectedStates; + + public TestStateListener(HttpConnection.State[] expectedStates, CountDownLatch latch, HttpRequest... httpRequests) { + this.httpRequests = httpRequests; + this.latch = latch; + this.expectedStates = new LinkedList<>(Arrays.asList(expectedStates)); + } + + @Override + public void onStateChanged(HttpConnection connection, HttpConnection.State oldState, HttpConnection.State newState) { + System.out.printf("Connection [%s] state change: %s -> %s\n", connection, oldState, newState); + + observedStates.add(newState); + + HttpConnection.State expectedState = expectedStates.poll(); + if (expectedState != newState) { + latch.countDown(); + } + + if (newState == IDLE && httpRequests.length > sentRequests.get()) { + connection.send(httpRequests[sentRequests.get()]); + sentRequests.incrementAndGet(); + } + + if (expectedStates.peek() == null) { + latch.countDown(); + } + } + + public List<HttpConnection.State> getObservedStates() { + return observedStates; + } + } + + private static class InterceptorFilter extends Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> { + + private final ERROR_STATE errorState; + + InterceptorFilter(Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> downstreamFilter, ERROR_STATE errroState) { + super(downstreamFilter); + this.errorState = errroState; + } + + @Override + void write(HttpRequest data, final CompletionHandler<HttpRequest> completionHandler) { + if (errorState == ERROR_STATE.LOST_REQUEST) { + completionHandler.completed(data); + return; + } + + if (errorState == ERROR_STATE.SENDING) { + completionHandler.failed(testError); + return; + } + + if (errorState == ERROR_STATE.RECEIVING_HEADER) { + downstreamFilter.write(data, new CompletionHandler<HttpRequest>() { + @Override + public void completed(HttpRequest result) { + completionHandler.completed(result); + } + }); + downstreamFilter.onError(testError); + return; + } + + downstreamFilter.write(data, completionHandler); + } + + @Override + void connect(SocketAddress address, Filter<?, ?, HttpRequest, HttpResponse> upstreamFilter) { + if (errorState == ERROR_STATE.LOST_CONNECT) { + return; + } + + if (errorState == ERROR_STATE.CONNECTING) { + return; + } + + super.connect(address, upstreamFilter); + } + } + + private enum ERROR_STATE { + NONE, + CONNECTING, + SENDING, + RECEIVING_HEADER, + LOST_REQUEST, + LOST_CONNECT + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/HttpParserTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/HttpParserTest.java new file mode 100644 index 0000000..b73a8bb --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/HttpParserTest.java
@@ -0,0 +1,525 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class HttpParserTest { + + private static final Charset responseEncoding = Charset.forName("ISO-8859-1"); + + private HttpParser httpParser; + + @Before + public void prepare() { + httpParser = new HttpParser(1000, 1000); + } + + @Test + public void testResponseLineInOnePiece() throws ParseException { + testResponseLine(Integer.MAX_VALUE); + } + + @Test + public void testResponseLineSegmented() throws ParseException { + testResponseLine(20); + } + + private void testResponseLine(int segmentSize) throws ParseException { + httpParser.reset(false); + feedParser("HTTP/1.1 123 A meaningful code\r\n\r\n", segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + HttpResponse httpResponse = httpParser.getHttpResponse(); + assertNotNull(httpResponse); + + assertEquals("HTTP/1.1", httpResponse.getProtocolVersion()); + assertEquals(123, httpResponse.getStatusCode()); + assertEquals("A meaningful code", httpResponse.getReasonPhrase()); + } + + @Test + public void testHeadersInOnePiece() throws ParseException { + testHeaders(Integer.MAX_VALUE); + } + + @Test + public void testHeadersSegmented() throws ParseException { + testHeaders(20); + } + + private void testHeaders(int segmentSize) throws ParseException { + httpParser.reset(false); + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("name2: value2\r\n\r\n"); + feedParser(request.toString(), segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("name2", "value2"); + } + + private void verifyHeaderValue(String name, String... expectedValues) { + verifyHeaderValue(name, false, expectedValues); + } + + private void verifyTrailerHeaderValue(String name, String... expectedValues) { + verifyHeaderValue(name, true, expectedValues); + } + + private void verifyHeaderValue(String name, boolean trailerHeader, String... expectedValues) { + HttpResponse httpResponse = httpParser.getHttpResponse(); + List<String> receivedValues; + + if (trailerHeader) { + receivedValues = httpResponse.getTrailerHeader(name); + } else { + receivedValues = httpResponse.getHeader(name); + } + + assertNotNull(receivedValues); + assertEquals(expectedValues.length, receivedValues.size()); + for (String expectedValue : expectedValues) { + assertTrue(receivedValues.contains(expectedValue)); + } + } + + @Test + public void testFixedLengthBodyInOnePiece() throws ParseException, IOException { + testFixedLengthBody(Integer.MAX_VALUE); + } + + @Test + public void testFixedLengthBodySegmented() throws ParseException, IOException { + testFixedLengthBody(20); + } + + private void testFixedLengthBody(int segmentSize) throws ParseException, IOException { + httpParser.reset(true); + + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("name2: value2\r\n") + .append("Content-Length: 56\r\n\r\n"); + + StringBuilder bodyBuilder = new StringBuilder(); + + for (int i = 0; i < 8; i++) { + bodyBuilder.append("ABCDEFG"); + } + + String body = bodyBuilder.toString(); + request.append(body); + + feedParser(request.toString(), segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyReceivedBody(body); + } + + @Test + public void testChunkedBodyInOnePiece() throws ParseException, IOException { + testChunkedBody(Integer.MAX_VALUE, 25, generateBody()); + } + + @Test + public void testChunkedBodySegmentedWithSmallChunk() throws ParseException, IOException { + testChunkedBody(20, 15, generateBody()); + } + + @Test + public void testChunkedBodySegmentedWithLargerChunk() throws ParseException, IOException { + testChunkedBody(20, 23, generateBody()); + } + + @Test + public void testEmptyChunkedBody() throws ParseException, IOException { + testChunkedBody(Integer.MAX_VALUE, 25, ""); + } + + private void testChunkedBody(int segmentSize, int chunkSize, String responseBody) throws ParseException, IOException { + httpParser.reset(true); + + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("name2: value2\r\n") + .append("Transfer-encoding: chunked\r\n\r\n"); + + String chunkedBody = encodeChunk(responseBody, chunkSize, new HashMap<>()); + request.append(chunkedBody); + + feedParser(request.toString(), segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyReceivedBody(responseBody); + } + + private String encodeChunk(String message, int chunkSize, Map<String, String> trailerHeaders) + throws UnsupportedEncodingException { + int messageLength = message.getBytes("ASCII").length; + int chunkStartIdx = 0; + + StringBuilder body = new StringBuilder(); + while (chunkStartIdx < messageLength) { + int chunkLength = chunkStartIdx + chunkSize < messageLength - 1 ? chunkSize : messageLength - chunkStartIdx; + body.append(Integer.toHexString(chunkLength)).append("\r\n"); + body.append(message.substring(chunkStartIdx, chunkStartIdx + chunkLength)); + body.append("\r\n"); + chunkStartIdx += chunkLength; + } + + body.append("0").append("\r\n"); + + for (Map.Entry<String, String> header : trailerHeaders.entrySet()) { + body.append(header.getKey()).append(": ").append(header.getValue()).append("\r\n"); + } + + body.append("\r\n"); + + return body.toString(); + } + + @Test + public void testMultilineHeaderInOnePiece() throws ParseException { + testMultilineHeader(Integer.MAX_VALUE); + } + + @Test + public void testMultilineHeaderSegmented() throws ParseException { + testMultilineHeader(10); + } + + private void testMultilineHeader(int segmentSize) throws ParseException { + httpParser.reset(false); + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("multi-line: first\r\n second\r\n third\r\n") + .append("name2: value2\r\n\r\n"); + feedParser(request.toString(), segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("name2", "value2"); + verifyHeaderValue("multi-line", "first second third"); + } + + @Test + public void testMultilineHeaderNInOnePiece() throws ParseException { + testMultilineHeaderN(Integer.MAX_VALUE); + } + + @Test + public void testMultilineHeaderNSegmented() throws ParseException { + testMultilineHeaderN(10); + } + + private void testMultilineHeaderN(int segmentSize) throws ParseException { + httpParser.reset(false); + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("multi-line: first\n second\n third\r\n") + .append("name2: value2\r\n\r\n"); + feedParser(request.toString(), segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("name2", "value2"); + verifyHeaderValue("multi-line", "first second third"); + } + + @Test + public void testOverflowProtocol() { + try { + testOverflow("HTTP/1.0 404 Not found\n\n", 2); + fail(); + } catch (ParseException e) { + assertTrue(true); + } + } + + @Test + public void testOverflowCode() { + try { + testOverflow("HTTP/1.0 404 Not found\n\n", 11); + fail(); + } catch (ParseException e) { + assertTrue(true); + } + } + + @Test + public void testOverflowPhrase() { + try { + testOverflow("HTTP/1.0 404 Not found\n\n", 19); + fail(); + } catch (ParseException e) { + assertTrue(true); + } + } + + @Test + public void testOverflowHeader() { + try { + testOverflow("HTTP/1.0 404 Not found\nHeader1: somevalue\n\n", 30); + fail(); + } catch (ParseException e) { + assertTrue(true); + } + } + + @Test + public void testTrailerHeadersInOnePiece() throws IOException, ParseException { + testTrailerHeaders(Integer.MAX_VALUE, 15); + } + + @Test + public void testTrailerHeadersSegmented() throws IOException, ParseException { + testTrailerHeaders(20, 15); + } + + @Test + public void testSpacesInChunkSizeHeader() throws Exception { + httpParser.reset(true); + + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 123 A meaningful code\r\n") + .append("Transfer-Encoding: chunked\r\n\r\n"); + + String body = "ABCDE"; + String bodyLen = Integer.toHexString(body.length()); + response.append(" ").append(bodyLen).append(" ").append("\r\n").append(body).append("\r\n"); + response.append(" 0 ").append("\r\n").append("\r\n"); + + feedParser(response.toString(), Integer.MAX_VALUE); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + verifyReceivedBody(body); + } + + /** + * This seems to be broken in Grizzly parser + */ + @Ignore + @Test + public void testChunkExtension() throws ParseException, IOException { + httpParser.reset(true); + + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 123 A meaningful code\r\n") + .append("Transfer-Encoding: chunked\r\n\r\n"); + + String body = "ABCDE"; + String bodyLen = Integer.toHexString(body.length()); + response.append(bodyLen).append(";extName=extValue").append("\r\n").append(body).append("\r\n"); + response.append("0;extName2=extValue2").append("\r\n").append("\r\n"); + + feedParser(response.toString(), Integer.MAX_VALUE); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + verifyReceivedBody(body); + } + + @Test + public void testSameHeaders() throws ParseException { + httpParser.reset(false); + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("name2: value2\r\n") + .append("name3: value3\r\n") + .append("name2: value4\r\n\r\n"); + feedParser(request.toString(), Integer.MAX_VALUE); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("name2", "value2", "value2"); + verifyHeaderValue("name3", "value3"); + } + + @Test + public void testSameHeadersCommaSeparated() throws ParseException { + httpParser.reset(false); + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("name2: value2, value4\r\n") + .append("name3: value3\r\n\r\n"); + feedParser(request.toString(), Integer.MAX_VALUE); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("name2", "value2", "value4"); + verifyHeaderValue("name3", "value3"); + } + + @Test + public void testInseparableHeaders() throws ParseException { + httpParser.reset(false); + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("WWW-Authenticate: value2, value4\r\n") + .append("name3: value3, value5\r\n\r\n"); + feedParser(request.toString(), Integer.MAX_VALUE); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("WWW-Authenticate", "value2, value4"); + verifyHeaderValue("name3", "value3", "value5"); + } + + private void testTrailerHeaders(int segmentSize, int chunkSize) throws IOException, ParseException { + httpParser.reset(true); + + StringBuilder request = new StringBuilder(); + request.append("HTTP/1.1 123 A meaningful code\r\n") + .append("name1: value1\r\n") + .append("name2: value2\r\n") + .append("Transfer-Encoding: chunked\r\n\r\n"); + + StringBuilder bodyBuilder = new StringBuilder(); + + for (int i = 0; i < 8; i++) { + bodyBuilder.append("ABCDEFG"); + } + + String body = bodyBuilder.toString(); + + Map<String, String> trailerHeaders = new HashMap<>(); + trailerHeaders.put("name3", "value3"); + trailerHeaders.put("name2", "value4"); + + String chunkedBody = encodeChunk(body, chunkSize, trailerHeaders); + request.append(chunkedBody); + + feedParser(request.toString(), segmentSize); + + assertTrue(httpParser.isHeaderParsed()); + assertTrue(httpParser.isComplete()); + + verifyHeaderValue("name1", "value1"); + verifyHeaderValue("name2", "value2"); + verifyTrailerHeaderValue("name3", "value3"); + verifyTrailerHeaderValue("name2", "value4"); + + verifyReceivedBody(body); + } + + private void testOverflow(String response, int maxHeaderSize) throws ParseException { + httpParser = new HttpParser(maxHeaderSize, Integer.MAX_VALUE); + httpParser.reset(false); + feedParser(response, Integer.MAX_VALUE); + } + + private void verifyReceivedBody(String sentMessage) throws IOException { + HttpResponse httpResponse = httpParser.getHttpResponse(); + AsynchronousBodyInputStream bodyStream = httpResponse.getBodyStream(); + + byte[] receivedBytes = new byte[sentMessage.getBytes("ASCII").length]; + int writeIdx = 0; + + while (true) { + byte b = (byte) bodyStream.read(); + if (b == (byte) -1) { + break; + } + + if (writeIdx == receivedBytes.length) { + fail(); + } + + receivedBytes[writeIdx] = b; + writeIdx++; + } + + String receivedMessage = new String(receivedBytes, "ASCII"); + assertEquals(sentMessage, receivedMessage); + } + + private void feedParser(String request, int segmentSize) throws ParseException { + List<ByteBuffer> serializedResponse = new ArrayList<>(); + byte[] bytes = request.getBytes(responseEncoding); + ByteBuffer bufferedResponse = ByteBuffer.wrap(bytes); + int segmentStartIdx = 0; + while (segmentStartIdx < bytes.length - 1) { + int segmentLength = segmentStartIdx + segmentSize < bytes.length - 1 ? segmentSize : bytes.length - segmentStartIdx; + byte[] segmentBytes = new byte[segmentLength]; + bufferedResponse.get(segmentBytes); + ByteBuffer segment = ByteBuffer.wrap(segmentBytes); + serializedResponse.add(segment); + segmentStartIdx += segmentLength; + } + + for (ByteBuffer input : serializedResponse) { + httpParser.parse(input); + } + } + + private String generateBody() { + StringBuilder bodyBuilder = new StringBuilder(); + + for (int i = 0; i < 8; i++) { + bodyBuilder.append("ABCDEFG"); + } + + return bodyBuilder.toString(); + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ModifyHeaderInBodyWriterTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ModifyHeaderInBodyWriterTest.java new file mode 100644 index 0000000..a3e6772 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ModifyHeaderInBodyWriterTest.java
@@ -0,0 +1,179 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class ModifyHeaderInBodyWriterTest extends JerseyTest { + + private static final String WRITE_BYTES = "write bytes"; + private static final String WRITE_BYTE = "byte"; + private static final String HEADER_NAME = "myHeader"; + private static final String HEADER_VALUE = "myHeaderValue"; + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class, HeaderModifyingWriter.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(HeaderModifyingWriter.class); + config.connectorProvider(new JdkConnectorProvider()); + } + + @Test + public void testBufferedWriteBytes() { + doTestWriteBytes(RequestEntityProcessing.BUFFERED); + } + + @Test + public void testChunkedWriteBytes() { + doTestWriteBytes(RequestEntityProcessing.CHUNKED); + } + + private void doTestWriteBytes(RequestEntityProcessing requestEntityProcessing) { + Response response = target("echo").request().property(ClientProperties.REQUEST_ENTITY_PROCESSING, requestEntityProcessing) + .post(Entity.entity(WRITE_BYTES, MediaType.TEXT_PLAIN)); + assertEquals(200, response.getStatus()); + assertEquals(HEADER_VALUE, response.getHeaderString(HEADER_NAME)); + assertEquals(WRITE_BYTES, response.readEntity(String.class)); + } + + @Test + public void testBufferedWriteByte() { + doTestWriteByte(RequestEntityProcessing.BUFFERED); + } + + @Test + public void testChunkedWriteByte() { + doTestWriteByte(RequestEntityProcessing.CHUNKED); + } + + private void doTestWriteByte(RequestEntityProcessing requestEntityProcessing) { + Response response = target("echo").request().property(ClientProperties.REQUEST_ENTITY_PROCESSING, requestEntityProcessing) + .post(Entity.entity(WRITE_BYTE, MediaType.TEXT_PLAIN)); + assertEquals(200, response.getStatus()); + assertEquals(HEADER_VALUE, response.getHeaderString(HEADER_NAME)); + assertEquals(WRITE_BYTE, response.readEntity(String.class)); + } + + @Test + public void testBufferedWriteNothing() { + doTestWriteNothing(RequestEntityProcessing.BUFFERED); + } + + @Test + public void testChunkedWriteNothing() { + doTestWriteNothing(RequestEntityProcessing.CHUNKED); + } + + private void doTestWriteNothing(RequestEntityProcessing requestEntityProcessing) { + Response response = target("echo").request().property(ClientProperties.REQUEST_ENTITY_PROCESSING, requestEntityProcessing) + .post(Entity.entity("", MediaType.TEXT_PLAIN)); + assertEquals(200, response.getStatus()); + assertEquals(HEADER_VALUE, response.getHeaderString(HEADER_NAME)); + assertEquals("", response.readEntity(String.class)); + } + + @Provider + @Produces("text/plain") + public static class HeaderModifyingWriter implements MessageBodyWriter<String> { + + @Override + public boolean isWriteable( + final Class<?> type, + final Type genericType, + final Annotation[] annotations, + final MediaType mediaType) { + return type == String.class; + } + + @Override + public long getSize( + final String t, + final Class<?> type, + final Type genericType, + final Annotation[] annotations, + final MediaType mediaType) { + return -1; + } + + @Override + public void writeTo( + final String t, + final Class<?> type, + final Type genericType, + final Annotation[] annotations, + final MediaType mediaType, + final MultivaluedMap<String, Object> httpHeaders, + final OutputStream entityStream) throws IOException, WebApplicationException { + + httpHeaders.putSingle(HEADER_NAME, HEADER_VALUE); + + if (WRITE_BYTES.equals(t)) { + entityStream.write(t.getBytes()); + } + + if (WRITE_BYTE.equals(t)) { + for (byte b : t.getBytes()) { + entityStream.write(b); + } + } + } + } + + @Path("/") + public static class Resource { + + @Path("echo") + @Produces("text/html") + @POST + public Response echo(String msg, @HeaderParam(HEADER_NAME) String header) { + return Response.ok().entity(msg).header(HEADER_NAME, HEADER_VALUE).build(); + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/MultiValueHeaderTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/MultiValueHeaderTest.java new file mode 100644 index 0000000..281ced5 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/MultiValueHeaderTest.java
@@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +public class MultiValueHeaderTest extends JerseyTest { + + @Path("testResource") + public static class TestResource { + @GET + public Response get(@Context HttpHeaders h) { + final String headerString = h.getHeaderString("tools"); + if ("hammer,axe,shovel,drill".equals(headerString)) { + return Response.ok(headerString) + .header("animals", "mole") + .header("animals", "hedgehog") + .header("animals", "bat") + .header("animals", "rabbit") + .build(); + } else { + return Response.serverError().entity(headerString).build(); + } + } + } + + @Override + protected Application configure() { + return new ResourceConfig(TestResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JdkConnectorProvider()); + } + + @Test + public void testCookieResource() { + // the default cookie policy does not like cookies from localhost + Response response = target("testResource").request() + .header("tools", "hammer") + .header("tools", "axe") + .header("tools", "shovel") + .header("tools", "drill") + .get(); + + Assert.assertEquals(200, response.getStatus()); + + final String values = response.getHeaderString("animals"); + Assert.assertEquals("mole,hedgehog,bat,rabbit", values); + } + +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ProxyTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ProxyTest.java new file mode 100644 index 0000000..d1a0a72 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ProxyTest.java
@@ -0,0 +1,320 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.internal.util.Base64; +import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.glassfish.grizzly.http.server.HttpHandler; +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class ProxyTest extends JerseyTest { + + private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1"); + + private static final String PROXY_HOST = "localhost"; + private static final int PROXY_PORT = 8321; + private static final String PROXY_USER_NAME = "petr"; + private static final String PROXY_PASSWORD = "my secret password"; + + @Test + public void testConnect() throws IOException { + doTest(Proxy.Authentication.NONE); + } + + @Test + public void testBasicAuthentication() throws IOException { + doTest(Proxy.Authentication.BASIC); + } + + @Test + public void testDigestAuthentication() throws IOException { + doTest(Proxy.Authentication.DIGEST); + } + + private void doTest(Proxy.Authentication authentication) throws IOException { + Proxy proxy = new Proxy(authentication); + try { + proxy.start(); + javax.ws.rs.core.Response response = target("resource").request().get(); + assertEquals(200, response.getStatus()); + assertEquals("OK", response.readEntity(String.class)); + assertTrue(proxy.getProxyHit()); + } finally { + proxy.stop(); + } + } + + @Test + public void authenticationFailTest() throws IOException { + Proxy proxy = new Proxy(Proxy.Authentication.BASIC); + try { + proxy.start(); + proxy.setAuthernticationFail(true); + try { + target("resource").request().get(); + fail(); + } catch (Exception e) { + assertEquals(ProxyAuthenticationException.class, e.getCause().getClass()); + } + + assertTrue(proxy.getProxyHit()); + } finally { + proxy.stop(); + } + } + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class); + } + + @Override + protected void configureClient(final ClientConfig config) { + config.property(JdkConnectorProperties.MAX_CONNECTIONS_PER_DESTINATION, 1); + config.property(ClientProperties.PROXY_URI, "http://" + PROXY_HOST + ":" + PROXY_PORT); + config.property(ClientProperties.PROXY_USERNAME, PROXY_USER_NAME); + config.property(ClientProperties.PROXY_PASSWORD, PROXY_PASSWORD); + config.connectorProvider(new JdkConnectorProvider()); + } + + @Path("/resource") + public static class Resource { + + @GET + public String get() { + return "OK"; + } + } + + private static class Proxy { + + private final HttpServer server = HttpServer.createSimpleServer("/", PROXY_HOST, PROXY_PORT); + private volatile String destinationUri = null; + private final Authentication authentication; + private volatile boolean proxyHit = false; + private volatile boolean authenticationFail = false; + + Proxy(Authentication authentication) { + this.authentication = authentication; + } + + boolean getProxyHit() { + return proxyHit; + } + + void setAuthernticationFail(boolean authenticationFail) { + this.authenticationFail = authenticationFail; + } + + void start() throws IOException { + server.getServerConfiguration().addHttpHandler(new HttpHandler() { + public void service(Request request, Response response) throws Exception { + if (request.getMethod().getMethodString().equals("CONNECT")) { + proxyHit = true; + + String authorizationHeader = request.getHeader("Proxy-Authorization"); + + if (authentication != Authentication.NONE && authorizationHeader == null) { + // if we need authentication and receive CONNECT with no Proxy-authorization header, send 407 + send407(request, response); + return; + } + + if (authenticationFail) { + send407(request, response); + return; + } + + if (authentication == Authentication.BASIC) { + if (!verifyBasicAuthorizationHeader(response, authorizationHeader)) { + return; + } + + // if success continue + + } else if (authentication == Authentication.DIGEST) { + if (!verifyDigestAuthorizationHeader(response, authorizationHeader)) { + return; + } + + // if success continue + } + + // check that both Host header and URI contain host:port + String requestURI = request.getRequestURI(); + String host = request.getHeader("Host"); + if (!requestURI.equals(host)) { + response.setStatus(400); + System.out.println("Request URI: " + requestURI); + System.out.println("Host header: " + host); + return; + } + + // save the destination where a normal proxy would open a connection + destinationUri = "http://" + requestURI; + response.setStatus(200); + hackGrizzlyConnect(request, response); + return; + } + + handleTrafficAfterConnect(request, response); + } + }); + + server.start(); + } + + private void send407(Request request, Response response) { + response.setStatus(407); + + if (authentication == Authentication.BASIC) { + response.setHeader("Proxy-Authenticate", "Basic"); + } else { + response.setHeader("Proxy-Authenticate", "Digest realm=\"my-realm\", domain=\"\", " + + "nonce=\"n9iv3MeSNkEfM3uJt2gnBUaWUbKAljxp\", algorithm=MD5, \"\n" + + " + \"qop=\"auth\", stale=false"); + } + hackGrizzlyConnect(request, response); + } + + private boolean verifyBasicAuthorizationHeader(Response response, String authorizationHeader) { + if (!authorizationHeader.startsWith("Basic")) { + System.out.println( + "Authorization header during Basic authentication does not start with \"Basic\""); + response.setStatus(400); + return false; + } + String decoded = new String(Base64.decode(authorizationHeader.substring(6).getBytes()), + CHARACTER_SET); + final String[] split = decoded.split(":"); + final String username = split[0]; + final String password = split[1]; + + if (!username.equals(PROXY_USER_NAME)) { + response.setStatus(400); + System.out.println("Found unexpected username: " + username); + return false; + } + + if (!password.equals(PROXY_PASSWORD)) { + response.setStatus(400); + System.out.println("Found unexpected password: " + username); + return false; + } + + return true; + } + + private boolean verifyDigestAuthorizationHeader(Response response, String authorizationHeader) { + if (!authorizationHeader.startsWith("Digest")) { + System.out.println( + "Authorization header during Digest authentication does not start with \"Digest\""); + response.setStatus(400); + return false; + } + + final Matcher match = Pattern.compile("username=\"([^\"]+)\"").matcher(authorizationHeader); + if (!match.find()) { + return false; + } + final String username = match.group(1); + if (!username.equals(PROXY_USER_NAME)) { + response.setStatus(400); + System.out.println("Found unexpected username: " + username); + return false; + } + + return true; + } + + private void hackGrizzlyConnect(Request request, Response response) { + // Grizzly does not like CONNECT method and sets keep alive to false + // This hacks Grizzly, so it will keep the connection open + response.getResponse().getProcessingState().setKeepAlive(true); + response.getResponse().setContentLength(0); + request.setMethod("GET"); + } + + private void handleTrafficAfterConnect(Request request, Response response) throws IOException { + if (destinationUri == null) { + // It seems that CONNECT has not been called + System.out.println("Received non-CONNECT without receiving CONNECT first"); + response.setStatus(400); + return; + } + + // create a client and relay the request to the final destination + ClientConfig clientConfig = new ClientConfig(); + clientConfig.connectorProvider(new JdkConnectorProvider()); + Client client = ClientBuilder.newClient(clientConfig); + + Invocation.Builder destinationRequest = client.target(destinationUri).path(request.getRequestURI()).request(); + for (String headerName : request.getHeaderNames()) { + destinationRequest.header(headerName, request.getHeader(headerName)); + } + + javax.ws.rs.core.Response destinationResponse = destinationRequest + .method(request.getMethod().getMethodString()); + + // translate the received response into the proxy response + response.setStatus(destinationResponse.getStatus()); + OutputStream outputStream = response.getOutputStream(); + String body = destinationResponse.readEntity(String.class); + outputStream.write(body.getBytes()); + client.close(); + } + + void stop() { + server.shutdown(); + } + + private enum Authentication { + NONE, + BASIC, + DIGEST + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/PublicSitesTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/PublicSitesTest.java new file mode 100644 index 0000000..4d77939 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/PublicSitesTest.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +@Ignore +public class PublicSitesTest extends JerseyTest { + + @Test + public void testGoolgeCom() throws InterruptedException { + doTest("https://www.google.com"); + } + + @Test + public void testSeznam() throws InterruptedException { + doTest("https://www.seznam.cz"); + } + + @Test + public void testGoogleUK() throws InterruptedException { + doTest("https://www.google.co.uk"); + } + + @Test + public void testWikipedia() throws InterruptedException { + doTest("http://www.wikipedia.com"); + } + + @Test + public void testJavaNet() throws InterruptedException { + doTest("http://www.java.net"); + } + + @Test + public void testTheGuardian() throws InterruptedException { + doTest("http://www.theguardian.com"); + } + + @Test + public void testBbcUk() throws InterruptedException { + doTest("http://www.bbc.co.uk"); + } + + @Test + public void testServis24() throws InterruptedException { + doTest("https://www.servis24.cz"); + } + + private void doTest(String url) { + Response response = client().target(url).request().get(); + String htmlPage = response.readEntity(String.class); + assertEquals(200, response.getStatus()); + assertTrue(htmlPage.contains("<html")); + assertTrue(htmlPage.contains("</html>")); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JdkConnectorProvider()); + } + + @Override + protected Application configure() { + return new ResourceConfig(); + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ReadChunkedEntity.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ReadChunkedEntity.java new file mode 100644 index 0000000..42e944b --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/ReadChunkedEntity.java
@@ -0,0 +1,104 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.IOException; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ChunkedOutput; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + * + * TODO I have a strong feeling a got inspired somewhere, but forgot where. + */ +public class ReadChunkedEntity extends JerseyTest { + + @Path("/chunkedEntity") + public static class ChunkedResource { + + @POST + public ChunkedOutput<String> get(final String entity) { + final ChunkedOutput<String> output = new ChunkedOutput<>(String.class); + + new Thread() { + public void run() { + try { + int startIdx = 0; + int remaining = entity.length(); + while (remaining >= 0) { + int chunkLength = 10; + int endIdx = startIdx + chunkLength; + if (endIdx > entity.length()) { + endIdx = entity.length(); + } + String chunk = entity.substring(startIdx, endIdx); + output.write(chunk); + remaining -= chunkLength; + startIdx = endIdx; + } + } catch (IOException e) { + // + } finally { + try { + output.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + } + }.start(); + + return output; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(ChunkedResource.class); + } + + @Test + public void testChunked() { + String message = TestUtils.generateBody(500); + Response response = target("chunkedEntity").property(ClientProperties.REQUEST_ENTITY_PROCESSING, + RequestEntityProcessing.CHUNKED).request().post(Entity.entity(message, MediaType.TEXT_PLAIN)); + assertEquals(message, response.readEntity(String.class)); + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 20); + config.connectorProvider(new JdkConnectorProvider()); + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/RedirectTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/RedirectTest.java new file mode 100644 index 0000000..1fb31eb --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/RedirectTest.java
@@ -0,0 +1,225 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class RedirectTest extends JerseyTest { + + private static String TARGET_GET_MSG = "You have reached the target"; + + @Override + protected Application configure() { + return new ResourceConfig(RedirectingResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JdkConnectorProvider()); + } + + @Test + public void testDisableRedirect() { + Response response = target("redirecting/303").property(ClientProperties.FOLLOW_REDIRECTS, false).request().get(); + assertEquals(303, response.getStatus()); + } + + @Test + public void testGet303() { + Response response = target("redirecting/303").request().get(); + assertEquals(200, response.getStatus()); + assertEquals(TARGET_GET_MSG, response.readEntity(String.class)); + } + + @Test + public void testPost303() { + Response response = target("redirecting/303").request().post(Entity.entity("My awesome message", MediaType.TEXT_PLAIN)); + assertEquals(200, response.getStatus()); + assertEquals(TARGET_GET_MSG, response.readEntity(String.class)); + } + + @Test + public void testHead303() { + Response response = target("redirecting/303").request().head(); + assertEquals(200, response.getStatus()); + assertTrue(response.readEntity(String.class).isEmpty()); + } + + // in this implementation; 301, 307 and 308 work exactly the same + @Test + public void testGet307() { + Response response = target("redirecting/307").request().get(); + assertEquals(200, response.getStatus()); + assertEquals(TARGET_GET_MSG, response.readEntity(String.class)); + } + + // in this implementation; 301, 307 and 308 work exactly the same + @Test + public void testPost307() { + Response response = target("redirecting/307").request().post(Entity.entity("My awesome message", MediaType.TEXT_PLAIN)); + assertEquals(307, response.getStatus()); + } + + // in this implementation; 301, 307 and 308 work exactly the same + @Test + public void testHead307() { + Response response = target("redirecting/307").request().head(); + assertEquals(200, response.getStatus()); + assertTrue(response.readEntity(String.class).isEmpty()); + } + + @Test + public void testCycle() { + try { + target("redirecting/cycle").request().get(); + fail(); + } catch (Throwable t) { + assertEquals(RedirectException.class.getName(), t.getCause().getClass().getName()); + } + } + + @Test + public void testMaxRedirectsSuccess() { + Response response = target("redirecting/maxRedirect").property(JdkConnectorProperties.MAX_REDIRECTS, 2).request().get(); + assertEquals(200, response.getStatus()); + assertEquals(TARGET_GET_MSG, response.readEntity(String.class)); + } + + @Test + public void testMaxRedirectsFail() { + try { + target("redirecting/maxRedirect").property(JdkConnectorProperties.MAX_REDIRECTS, 1).request().get(); + fail(); + } catch (Throwable t) { + assertEquals(RedirectException.class.getName(), t.getCause().getClass().getName()); + } + } + + @Path("/redirecting") + public static class RedirectingResource { + + private Response get303RedirectToTarget() { + return Response.seeOther(UriBuilder.fromResource(RedirectingResource.class).path("target").build()).build(); + } + + private Response get307RedirectToTarget() { + return Response.temporaryRedirect(UriBuilder.fromResource(RedirectingResource.class).path("target").build()).build(); + } + + @Path("303") + @HEAD + public Response head303() { + return get303RedirectToTarget(); + } + + @Path("303") + @GET + public Response get303() { + return get303RedirectToTarget(); + } + + @Path("303") + @POST + public Response post303(String entity) { + return get303RedirectToTarget(); + } + + @Path("307") + @HEAD + public Response head307() { + return get307RedirectToTarget(); + } + + @Path("307") + @GET + public Response get307() { + return get307RedirectToTarget(); + } + + @Path("307") + @POST + public Response post307(String entity) { + return get307RedirectToTarget(); + } + + @Path("target") + @GET + public String target() { + return TARGET_GET_MSG; + } + + @Path("target") + @POST + public String target(String entity) { + return entity; + } + + @Path("cycle") + @GET + public Response cycle() { + return Response.seeOther(UriBuilder.fromResource(RedirectingResource.class).path("cycleNode2").build()).build(); + } + + @Path("cycleNode2") + @GET + public Response cycleNode2() { + return Response.seeOther(UriBuilder.fromResource(RedirectingResource.class).path("cycleNode3").build()).build(); + } + + @Path("cycleNode3") + @GET + public Response cycleNode3() { + return Response.seeOther(UriBuilder.fromResource(RedirectingResource.class).path("cycle").build()).build(); + } + + @Path("maxRedirect") + @GET + public Response maxRedirect() { + return Response.seeOther(UriBuilder.fromResource(RedirectingResource.class).path("maxRedirectNode2").build()).build(); + } + + @Path("maxRedirectNode2") + @GET + public Response maxRedirectNode2() { + return Response.seeOther(UriBuilder.fromResource(RedirectingResource.class).path("target").build()).build(); + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java new file mode 100644 index 0000000..099c23c --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java
@@ -0,0 +1,536 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +import org.glassfish.jersey.SslConfigurator; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +public class SslFilterTest { + + private static final int PORT = 8321; + + @Before + public void beforeTest() { + System.setProperty("javax.net.ssl.keyStore", this.getClass().getResource("/keystore_server").getPath()); + System.setProperty("javax.net.ssl.keyStorePassword", "asdfgh"); + System.setProperty("javax.net.ssl.trustStore", this.getClass().getResource("/truststore_server").getPath()); + System.setProperty("javax.net.ssl.trustStorePassword", "asdfgh"); + } + + @Test + public void testBasicEcho() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.start(); + String message = "Hello world\n"; + ByteBuffer readBuffer = ByteBuffer.allocate(message.length()); + Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = openClientSocket("localhost", readBuffer, latch, + null); + + clientSocket.write(stringToBuffer(message), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + clientSocket.close(); + readBuffer.flip(); + String received = bufferToString(readBuffer); + assertEquals(message, received); + } finally { + server.stop(); + } + } + + @Test + public void testEcho100k() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.start(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + } + String message = sb.toString() + "\n"; + ByteBuffer readBuffer = ByteBuffer.allocate(message.length()); + Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = openClientSocket("localhost", readBuffer, latch, + null); + + clientSocket.write(stringToBuffer(message), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + clientSocket.close(); + readBuffer.flip(); + String received = bufferToString(readBuffer); + assertEquals(message, received); + } finally { + server.stop(); + } + } + + /** + * Like {@link #testBasicEcho()}, but the conversation is terminated by the server. + */ + @Test + public void testCloseServer() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.start(); + String message = "Hello world\n"; + ByteBuffer readBuffer = ByteBuffer.allocate(message.length()); + Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = openClientSocket("localhost", readBuffer, latch, + null); + + clientSocket.write(stringToBuffer(message), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + server.stop(); + readBuffer.flip(); + String received = bufferToString(readBuffer); + assertEquals(message, received); + } finally { + server.stop(); + } + } + + /** + * Test SSL re-handshake triggered by the server. + * <p/> + * Sends a short message. When the message has been sent by the client, the server triggers re-handshake + * and the client send a long message to make sure the re-handshake is performed during application data flow. + */ + @Test + public void testRehandshakeServer() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + final SslEchoServer server = new SslEchoServer(); + try { + server.start(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + } + String message1 = "Hello"; + String message2 = sb.toString() + "\n"; + ByteBuffer readBuffer = ByteBuffer.allocate(message1.length() + message2.length()); + final CountDownLatch message1Latch = new CountDownLatch(1); + Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = openClientSocket("localhost", readBuffer, latch, + null); + + clientSocket.write(stringToBuffer(message1), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + + @Override + public void completed(ByteBuffer result) { + try { + message1Latch.countDown(); + server.rehandshake(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + + assertTrue(message1Latch.await(5, TimeUnit.SECONDS)); + + clientSocket.write(stringToBuffer(message2), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + clientSocket.close(); + readBuffer.flip(); + String received = bufferToString(readBuffer); + assertEquals(message1 + message2, received); + } finally { + server.stop(); + } + } + + /** + * Test SSL re-handshake triggered by the client. + * <p/> + * The same as {@link #testRehandshakeServer()} except, the client starts re-handshake this time. + */ + @Test + public void testRehandshakeClient() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + final SslEchoServer server = new SslEchoServer(); + try { + server.start(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); + } + String message1 = "Hello"; + String message2 = sb.toString() + "\n"; + ByteBuffer readBuffer = ByteBuffer.allocate(message1.length() + message2.length()); + final CountDownLatch message1Latch = new CountDownLatch(1); + final Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = openClientSocket("localhost", readBuffer, + latch, null); + + clientSocket.write(stringToBuffer(message1), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + + @Override + public void completed(ByteBuffer result) { + message1Latch.countDown(); + // startSsl is overloaded in the test so it will start re-handshake, calling startSsl on a filter + // for a second time will not normally cause a re-handshake + clientSocket.startSsl(); + } + }); + + assertTrue(message1Latch.await(5, TimeUnit.SECONDS)); + + clientSocket.write(stringToBuffer(message2), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + clientSocket.close(); + readBuffer.flip(); + String received = bufferToString(readBuffer); + assertEquals(message1 + message2, received); + } finally { + server.stop(); + } + } + + @Test + public void testHostameVerificationFail() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.start(); + System.out.println("=== SSLHandshakeException (certificate_unknown) on the server expected ==="); + openClientSocket("127.0.0.1", ByteBuffer.allocate(0), latch, null); + fail(); + } catch (SSLException e) { + // expected + } finally { + server.stop(); + } + } + + @Test + public void testCustomHostameVerificationFail() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.start(); + HostnameVerifier verifier = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return false; + } + }; + + openClientSocket("localhost", ByteBuffer.allocate(0), latch, verifier); + fail(); + } catch (SSLException e) { + // expected + } finally { + server.stop(); + } + } + + @Test + public void testCustomHostameVerificationPass() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.start(); + HostnameVerifier verifier = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + + openClientSocket("127.0.0.1", ByteBuffer.allocate(0), latch, verifier); + } finally { + server.stop(); + } + } + + @Test + public void testClientAuthentication() throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + SslEchoServer server = new SslEchoServer(); + try { + server.setClientAuthentication(); + server.start(); + String message = "Hello world\n"; + ByteBuffer readBuffer = ByteBuffer.allocate(message.length()); + final Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = openClientSocket("localhost", readBuffer, + latch, null); + + clientSocket.write(stringToBuffer(message), new CompletionHandler<ByteBuffer>() { + @Override + public void failed(Throwable t) { + t.printStackTrace(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + clientSocket.close(); + readBuffer.flip(); + String received = bufferToString(readBuffer); + assertEquals(message, received); + } finally { + server.stop(); + } + } + + private String bufferToString(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + return new String(bytes); + } + + private ByteBuffer stringToBuffer(String string) { + byte[] bytes = string.getBytes(); + return ByteBuffer.wrap(bytes); + } + + /** + * Creates an SSL client. Returns when SSL handshake has been completed. + * + * @param completionLatch latch that will be triggered when the expected number of bytes has been received. + * @param readBuffer buffer where received message will be written. Must be the size of the expected message, + * because when it is filled {@code completionLatch} will be triggered. + * @throws Throwable any exception that occurs until SSL handshake has completed. + */ + private Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> openClientSocket(String host, + final ByteBuffer readBuffer, + final CountDownLatch completionLatch, + HostnameVerifier customHostnameVerifier) + throws Throwable { + SslConfigurator sslConfig = SslConfigurator.newInstance() + .trustStoreFile(this.getClass().getResource("/truststore_client").getPath()) + .trustStorePassword("asdfgh") + .keyStoreFile(this.getClass().getResource("/keystore_client").getPath()) + .keyStorePassword("asdfgh"); + + TransportFilter transportFilter = new TransportFilter(17_000, ThreadPoolConfig.defaultConfig(), 100_000); + final SslFilter sslFilter = new SslFilter(transportFilter, sslConfig.createSSLContext(), host, customHostnameVerifier); + + // exceptions errors that occur before SSL handshake has finished are thrown from this method + final AtomicReference<Throwable> exception = new AtomicReference<>(); + final CountDownLatch connectLatch = new CountDownLatch(1); + final CountDownLatch startSslLatch = new CountDownLatch(1); + Filter<ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer> clientSocket = new Filter<ByteBuffer, ByteBuffer, ByteBuffer, + ByteBuffer>( + sslFilter) { + + @Override + void processConnect() { + connectLatch.countDown(); + } + + @Override + boolean processRead(ByteBuffer data) { + readBuffer.put(data); + if (!readBuffer.hasRemaining()) { + completionLatch.countDown(); + } + return false; + } + + @Override + void startSsl() { + if (startSslLatch.getCount() == 1) { + downstreamFilter.startSsl(); + try { + startSslLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + sslFilter.rehandshake(); + } + } + + @Override + void processSslHandshakeCompleted() { + startSslLatch.countDown(); + } + + @Override + void processError(Throwable t) { + if (connectLatch.getCount() == 1 || startSslLatch.getCount() == 1) { + exception.set(t); + connectLatch.countDown(); + startSslLatch.countDown(); + } + } + + @Override + void write(ByteBuffer data, CompletionHandler<ByteBuffer> completionHandler) { + downstreamFilter.write(data, completionHandler); + } + + @Override + void processConnectionClosed() { + downstreamFilter.close(); + } + + @Override + void close() { + downstreamFilter.close(); + } + }; + + clientSocket.connect(new InetSocketAddress(host, PORT), null); + try { + connectLatch.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + clientSocket.startSsl(); + if (exception.get() != null) { + clientSocket.close(); + throw exception.get(); + } + + return clientSocket; + } + + /** + * SSL echo server. It expects a message to be terminated with \n. + */ + private static class SslEchoServer { + + private final ServerSocket serverSocket; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + private volatile SSLSocket socket; + private volatile boolean stopped = false; + + SslEchoServer() throws IOException { + ServerSocketFactory socketFactory = SSLServerSocketFactory.getDefault(); + serverSocket = socketFactory.createServerSocket(PORT); + + } + + void setClientAuthentication() { + ((SSLServerSocket) serverSocket).setNeedClientAuth(true); + } + + void start() { + executorService.execute(new Runnable() { + @Override + public void run() { + try { + socket = (SSLSocket) serverSocket.accept(); + InputStream inputStream = socket.getInputStream(); + + OutputStream outputStream = new BufferedOutputStream(socket.getOutputStream(), 100); + + while (!stopped) { + int result = inputStream.read(); + if (result == -1) { + return; + } + outputStream.write(result); + // '\n' indicates end of the client message + if (result == '\n') { + outputStream.flush(); + return; + } + } + + } catch (IOException e) { + if (!e.getClass().equals(SocketException.class)) { + e.printStackTrace(); + } + } + } + }); + } + + void stop() throws IOException { + executorService.shutdown(); + serverSocket.close(); + if (socket != null) { + socket.close(); + } + } + + void rehandshake() throws IOException { + socket.startHandshake(); + } + } +}
diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/TestUtils.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/TestUtils.java new file mode 100644 index 0000000..0312a48 --- /dev/null +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/TestUtils.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdk.connector.internal; + +/** + * @author Petr Janouch (petr.janouch at oracle.com) + */ +class TestUtils { + + static String generateBody(int size) { + String pattern = "ABCDEFG"; + StringBuilder bodyBuilder = new StringBuilder(); + + int fullIterations = size / pattern.length(); + for (int i = 0; i < fullIterations; i++) { + bodyBuilder.append(pattern); + } + + String remaining = pattern.substring(0, size - pattern.length() * fullIterations); + bodyBuilder.append(remaining); + return bodyBuilder.toString(); + } +}
diff --git a/connectors/jdk-connector/src/test/resources/client.cert b/connectors/jdk-connector/src/test/resources/client.cert new file mode 100644 index 0000000..d43d88b --- /dev/null +++ b/connectors/jdk-connector/src/test/resources/client.cert
@@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIIDJTCCAuOgAwIBAgIET5ZyYjALBgcqhkjOOAQDBQAwdjELMAkGA1UEBhMCQ1oxFzAVBgNVBAgT +DkN6ZWNoIFJlcHVibGljMQ8wDQYDVQQHEwZQcmFndWUxGzAZBgNVBAoTEk9yYWNsZSBDb3Jwb3Jh +dGlvbjEPMA0GA1UECxMGSmVyc2V5MQ8wDQYDVQQDEwZDbGllbnQwHhcNMTIwNDI0MDkyOTA2WhcN +MTIwNzIzMDkyOTA2WjB2MQswCQYDVQQGEwJDWjEXMBUGA1UECBMOQ3plY2ggUmVwdWJsaWMxDzAN +BgNVBAcTBlByYWd1ZTEbMBkGA1UEChMST3JhY2xlIENvcnBvcmF0aW9uMQ8wDQYDVQQLEwZKZXJz +ZXkxDzANBgNVBAMTBkNsaWVudDCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQD9f1OBHXUSKVLfSpwu +7OTn9hG3UjzvRADDHj+AtlEmaUVdQCJR+1k9jVj6v8X1ujD2y5tVbNeBO4AdNG/yZmC3a5lQpaSf +n+gEexAiwk+7qdf+t8Yb+DtX58aophUPBPuD9tPFHsMCNVQTWhaRMvZ1864rYdcq7/IiAxmd0UgB +xwIVAJdgUI8VIwvMspK5gqLrhAvwWBz1AoGBAPfhoIXWmz3ey7yrXDa4V7l5lK+7+jrqgvlXTAs9 +B4JnUVlXjrrUWU/mcQcQgYC0SRZxI+hMKBYTt88JMozIpuE8FnqLVHyNKOCjrh4rs6Z1kW6jfwv6 +ITVi8ftiegEkO8yk8b6oUZCJqIPf4VrlnwaSi2ZegHtVJWQBTDv+z0kqA4GEAAKBgBmHNACDk1aw +vUZjsRecMSBlkkCSqr/cCrYOsNwpfleQKsM6rdOofujANUVeoUFhX8e8K45FknxEqAugmhGQ9NRn +uMenrvV+XupC0V2uGH0OciXeAzHbfeItBCbmJcvMdPW/q+I2vFchv6+ajEiNHogBrCc3qwSMhyVQ +ug2fXHmJMAsGByqGSM44BAMFAAMvADAsAhQYznYmH0hrcLni4EqX3Ovac+pNJgIUehnEaW1V5djn +dhYBAYUkSycETl4= +-----END CERTIFICATE-----
diff --git a/connectors/jdk-connector/src/test/resources/keystore_client b/connectors/jdk-connector/src/test/resources/keystore_client new file mode 100644 index 0000000..d016fd2 --- /dev/null +++ b/connectors/jdk-connector/src/test/resources/keystore_client Binary files differ
diff --git a/connectors/jdk-connector/src/test/resources/keystore_server b/connectors/jdk-connector/src/test/resources/keystore_server new file mode 100644 index 0000000..a7c93fc --- /dev/null +++ b/connectors/jdk-connector/src/test/resources/keystore_server Binary files differ
diff --git a/connectors/jdk-connector/src/test/resources/server.cert b/connectors/jdk-connector/src/test/resources/server.cert new file mode 100644 index 0000000..820841c --- /dev/null +++ b/connectors/jdk-connector/src/test/resources/server.cert
@@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIIDKzCCAumgAwIBAgIET5ZyzjALBgcqhkjOOAQDBQAweTELMAkGA1UEBhMCQ1oxFzAVBgNVBAgT +DkN6ZWNoIFJlcHVibGljMQ8wDQYDVQQHEwZQcmFndWUxGzAZBgNVBAoTEk9yYWNsZSBDb3Jwb3Jh +dGlvbjEPMA0GA1UECxMGSmVyc2V5MRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTIwNDI0MDkzMDU0 +WhcNMTIwNzIzMDkzMDU0WjB5MQswCQYDVQQGEwJDWjEXMBUGA1UECBMOQ3plY2ggUmVwdWJsaWMx +DzANBgNVBAcTBlByYWd1ZTEbMBkGA1UEChMST3JhY2xlIENvcnBvcmF0aW9uMQ8wDQYDVQQLEwZK +ZXJzZXkxEjAQBgNVBAMTCWxvY2FsaG9zdDCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQD9f1OBHXUS +KVLfSpwu7OTn9hG3UjzvRADDHj+AtlEmaUVdQCJR+1k9jVj6v8X1ujD2y5tVbNeBO4AdNG/yZmC3 +a5lQpaSfn+gEexAiwk+7qdf+t8Yb+DtX58aophUPBPuD9tPFHsMCNVQTWhaRMvZ1864rYdcq7/Ii +Axmd0UgBxwIVAJdgUI8VIwvMspK5gqLrhAvwWBz1AoGBAPfhoIXWmz3ey7yrXDa4V7l5lK+7+jrq +gvlXTAs9B4JnUVlXjrrUWU/mcQcQgYC0SRZxI+hMKBYTt88JMozIpuE8FnqLVHyNKOCjrh4rs6Z1 +kW6jfwv6ITVi8ftiegEkO8yk8b6oUZCJqIPf4VrlnwaSi2ZegHtVJWQBTDv+z0kqA4GEAAKBgGIs +VTo7dODp6iOyHFL+mkOVlOloZcymyWlHUZXzKqrvAi5jISptZZM+AoJcUUlUWEO9uwVTvX0MCk+4 +viwlPwt+XhaPM0kqfFcx1IS07BAx7z9cXREfYQpoFSsFW7pUs6cdvu0rjj8Ip6BnHALxQDgaBk40 +zXM39kB9LdGBt4uDMAsGByqGSM44BAMFAAMvADAsAhQE4QoQP4xPibjnozo8x5ORJqBuCAIUTkLQ +2udZ2DeknwPYXp/zMkYXLN4= +-----END CERTIFICATE-----
diff --git a/connectors/jdk-connector/src/test/resources/truststore_client b/connectors/jdk-connector/src/test/resources/truststore_client new file mode 100644 index 0000000..74784fb --- /dev/null +++ b/connectors/jdk-connector/src/test/resources/truststore_client Binary files differ
diff --git a/connectors/jdk-connector/src/test/resources/truststore_server b/connectors/jdk-connector/src/test/resources/truststore_server new file mode 100644 index 0000000..9b26ce4 --- /dev/null +++ b/connectors/jdk-connector/src/test/resources/truststore_server Binary files differ
diff --git a/connectors/jetty-connector/pom.xml b/connectors/jetty-connector/pom.xml new file mode 100644 index 0000000..22e28ba --- /dev/null +++ b/connectors/jetty-connector/pom.xml
@@ -0,0 +1,101 @@ +<?xml version="1.0"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-jetty-connector</artifactId> + <packaging>jar</packaging> + <name>jersey-connectors-jetty</name> + + <description>Jersey Client Transport via Jetty</description> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-client</artifactId> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jetty-http</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-jetty</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>testsSkipJdk6</id> + <activation> + <jdk>1.6</jdk> + </activation> + <properties> + <skip.tests>true</skip.tests> + </properties> + </profile> + </profiles> + +</project>
diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyClientProperties.java b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyClientProperties.java new file mode 100644 index 0000000..e38fbb5 --- /dev/null +++ b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyClientProperties.java
@@ -0,0 +1,95 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.Map; + +import org.glassfish.jersey.internal.util.PropertiesClass; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +/** + * Configuration options specific to the Client API that utilizes {@link JettyConnectorProvider}. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +@PropertiesClass +public final class JettyClientProperties { + + /** + * Prevents instantiation. + */ + private JettyClientProperties() { + throw new AssertionError("No instances allowed."); + } + + /** + * A value of {@code false} indicates the client should handle cookies + * automatically using HttpClient's default cookie policy. A value + * of {@code false} will cause the client to ignore all cookies. + * <p/> + * The value MUST be an instance of {@link java.lang.Boolean}. + * If the property is absent the default value is {@code false} + */ + public static final String DISABLE_COOKIES = + "jersey.config.jetty.client.disableCookies"; + + /** + * The credential provider that should be used to retrieve + * credentials from a user. + * + * If an {@link org.eclipse.jetty.client.api.Authentication} mechanism is found, + * it is then used for the given request, returning an {@link org.eclipse.jetty.client.api.Authentication.Result}, + * which is then stored in the {@link org.eclipse.jetty.client.api.AuthenticationStore} + * so that subsequent requests can be preemptively authenticated. + + * <p/> + * The value MUST be an instance of {@link + * org.eclipse.jetty.client.util.BasicAuthentication}. If + * the property is absent a default provider will be used. + */ + public static final String PREEMPTIVE_BASIC_AUTHENTICATION = + "jersey.config.jetty.client.preemptiveBasicAuthentication"; + + /** + * A value of {@code false} indicates the client disable a hostname verification + * during SSL Handshake. A client will ignore CN value defined in a certificate + * that is stored in a truststore. + * <p/> + * The value MUST be an instance of {@link java.lang.Boolean}. + * If the property is absent the default value is {@code true} + */ + public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = + "jersey.config.jetty.client.enableSslHostnameVerification"; + + /** + * Get the value of the specified property. + * + * If the property is not set or the real value type is not compatible with the specified value type, returns {@code null}. + * + * @param properties Map of properties to get the property value from. + * @param key Name of the property. + * @param type Type to retrieve the value as. + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, final String key, final Class<T> type) { + return PropertiesHelper.getValue(properties, key, type, null); + } + +}
diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java new file mode 100644 index 0000000..68886e7 --- /dev/null +++ b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java
@@ -0,0 +1,469 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CookieStore; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.MultivaluedMap; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.internal.util.collection.ByteBufferInputStream; +import org.glassfish.jersey.internal.util.collection.NonBlockingInputStream; +import org.glassfish.jersey.message.internal.HeaderUtils; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.message.internal.Statuses; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.ProxyConfiguration; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.client.util.OutputStreamContentProvider; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.HttpCookieStore; +import org.eclipse.jetty.util.Jetty; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +/** + * A {@link Connector} that utilizes the Jetty HTTP Client to send and receive + * HTTP request and responses. + * <p/> + * The following properties are only supported at construction of this class: + * <ul> + * <li>{@link ClientProperties#ASYNC_THREADPOOL_SIZE}</li> + * <li>{@link ClientProperties#CONNECT_TIMEOUT}</li> + * <li>{@link ClientProperties#FOLLOW_REDIRECTS}</li> + * <li>{@link ClientProperties#PROXY_URI}</li> + * <li>{@link ClientProperties#PROXY_USERNAME}</li> + * <li>{@link ClientProperties#PROXY_PASSWORD}</li> + * <li>{@link ClientProperties#PROXY_PASSWORD}</li> + * <li>{@link JettyClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li> + * <li>{@link JettyClientProperties#DISABLE_COOKIES}</li> + * </ul> + * <p/> + * This transport supports both synchronous and asynchronous processing of client requests. + * The following methods are supported: GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, CONNECT and MOVE. + * <p/> + * Typical usage: + * <p/> + * <pre> + * {@code + * ClientConfig config = new ClientConfig(); + * Connector connector = new JettyConnector(config); + * config.connector(connector); + * Client client = ClientBuilder.newClient(config); + * + * // async request + * WebTarget target = client.target("http://localhost:8080"); + * Future<Response> future = target.path("resource").request().async().get(); + * + * // wait for 3 seconds + * Response response = future.get(3, TimeUnit.SECONDS); + * String entity = response.readEntity(String.class); + * client.close(); + * } + * </pre> + * <p> + * This connector supports only {@link org.glassfish.jersey.client.RequestEntityProcessing#BUFFERED entity buffering}. + * Defining the property {@link ClientProperties#REQUEST_ENTITY_PROCESSING} has no effect on this connector. + * </p> + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +class JettyConnector implements Connector { + + private static final Logger LOGGER = Logger.getLogger(JettyConnector.class.getName()); + + private final HttpClient client; + private final CookieStore cookieStore; + + /** + * Create the new Jetty client connector. + * + * @param jaxrsClient JAX-RS client instance, for which the connector is created. + * @param config client configuration. + */ + JettyConnector(final Client jaxrsClient, final Configuration config) { + final SSLContext sslContext = jaxrsClient.getSslContext(); + final SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setSslContext(sslContext); + + Boolean enableHostnameVerification = (Boolean) config.getProperties() + .get(JettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION); + if (enableHostnameVerification != null && enableHostnameVerification) { + sslContextFactory.setEndpointIdentificationAlgorithm("https"); + } + + this.client = new HttpClient(sslContextFactory); + + final Object connectTimeout = config.getProperties().get(ClientProperties.CONNECT_TIMEOUT); + if (connectTimeout != null && connectTimeout instanceof Integer && (Integer) connectTimeout > 0) { + client.setConnectTimeout((Integer) connectTimeout); + } + final Object threadPoolSize = config.getProperties().get(ClientProperties.ASYNC_THREADPOOL_SIZE); + if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { + final String name = HttpClient.class.getSimpleName() + "@" + hashCode(); + final QueuedThreadPool threadPool = new QueuedThreadPool((Integer) threadPoolSize); + threadPool.setName(name); + client.setExecutor(threadPool); + } + Boolean disableCookies = (Boolean) config.getProperties().get(JettyClientProperties.DISABLE_COOKIES); + disableCookies = (disableCookies != null) ? disableCookies : false; + + final AuthenticationStore auth = client.getAuthenticationStore(); + final Object basicAuthProvider = config.getProperty(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION); + if (basicAuthProvider != null && (basicAuthProvider instanceof BasicAuthentication)) { + auth.addAuthentication((BasicAuthentication) basicAuthProvider); + } + + final Object proxyUri = config.getProperties().get(ClientProperties.PROXY_URI); + if (proxyUri != null) { + final URI u = getProxyUri(proxyUri); + final ProxyConfiguration proxyConfig = client.getProxyConfiguration(); + proxyConfig.getProxies().add(new HttpProxy(u.getHost(), u.getPort())); + } + + if (disableCookies) { + client.setCookieStore(new HttpCookieStore.Empty()); + } + + try { + client.start(); + } catch (final Exception e) { + throw new ProcessingException("Failed to start the client.", e); + } + this.cookieStore = client.getCookieStore(); + } + + @SuppressWarnings("ChainOfInstanceofChecks") + private static URI getProxyUri(final Object proxy) { + if (proxy instanceof URI) { + return (URI) proxy; + } else if (proxy instanceof String) { + return URI.create((String) proxy); + } else { + throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI)); + } + } + + /** + * Get the {@link HttpClient}. + * + * @return the {@link HttpClient}. + */ + @SuppressWarnings("UnusedDeclaration") + public HttpClient getHttpClient() { + return client; + } + + /** + * Get the {@link CookieStore}. + * + * @return the {@link CookieStore} instance or null when + * JettyClientProperties.DISABLE_COOKIES set to true. + */ + public CookieStore getCookieStore() { + return cookieStore; + } + + @Override + public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException { + final Request jettyRequest = translateRequest(jerseyRequest); + final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); + final ContentProvider entity = getBytesProvider(jerseyRequest); + if (entity != null) { + jettyRequest.content(entity); + } + + try { + final ContentResponse jettyResponse = jettyRequest.send(); + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(), + JettyConnector.this.getClass().getName()); + + final javax.ws.rs.core.Response.StatusType status = jettyResponse.getReason() == null + ? Statuses.from(jettyResponse.getStatus()) + : Statuses.from(jettyResponse.getStatus(), jettyResponse.getReason()); + + final ClientResponse jerseyResponse = new ClientResponse(status, jerseyRequest); + processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse); + try { + jerseyResponse.setEntityStream(new HttpClientResponseInputStream(jettyResponse)); + } catch (final IOException e) { + LOGGER.log(Level.SEVERE, null, e); + } + + return jerseyResponse; + } catch (final Exception e) { + throw new ProcessingException(e); + } + } + + private static void processResponseHeaders(final HttpFields respHeaders, final ClientResponse jerseyResponse) { + for (final HttpField header : respHeaders) { + final String headerName = header.getName(); + final MultivaluedMap<String, String> headers = jerseyResponse.getHeaders(); + List<String> list = headers.get(headerName); + if (list == null) { + list = new ArrayList<>(); + } + list.add(header.getValue()); + headers.put(headerName, list); + } + } + + private static final class HttpClientResponseInputStream extends FilterInputStream { + + HttpClientResponseInputStream(final ContentResponse jettyResponse) throws IOException { + super(getInputStream(jettyResponse)); + } + + private static InputStream getInputStream(final ContentResponse response) { + return new ByteArrayInputStream(response.getContent()); + } + } + + private Request translateRequest(final ClientRequest clientRequest) { + + final URI uri = clientRequest.getUri(); + final Request request = client.newRequest(uri); + request.method(clientRequest.getMethod()); + + request.followRedirects(clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true)); + final Object readTimeout = clientRequest.getConfiguration().getProperties().get(ClientProperties.READ_TIMEOUT); + if (readTimeout != null && readTimeout instanceof Integer && (Integer) readTimeout > 0) { + request.timeout((Integer) readTimeout, TimeUnit.MILLISECONDS); + } + return request; + } + + private static Map<String, String> writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, final Request request) { + final Map<String, String> stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers); + + // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version) + request.getHeaders().remove(HttpHeader.USER_AGENT); + for (final Map.Entry<String, String> e : stringHeaders.entrySet()) { + request.getHeaders().add(e.getKey(), e.getValue()); + } + return stringHeaders; + } + + private ContentProvider getBytesProvider(final ClientRequest clientRequest) { + final Object entity = clientRequest.getEntity(); + + if (entity == null) { + return null; + } + + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { + @Override + public OutputStream getOutputStream(final int contentLength) throws IOException { + return outputStream; + } + }); + + try { + clientRequest.writeEntity(); + } catch (final IOException e) { + throw new ProcessingException("Failed to write request entity.", e); + } + return new BytesContentProvider(outputStream.toByteArray()); + } + + private ContentProvider getStreamProvider(final ClientRequest clientRequest) { + final Object entity = clientRequest.getEntity(); + + if (entity == null) { + return null; + } + + final OutputStreamContentProvider streamContentProvider = new OutputStreamContentProvider(); + clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { + @Override + public OutputStream getOutputStream(final int contentLength) throws IOException { + return streamContentProvider.getOutputStream(); + } + }); + return streamContentProvider; + } + + private void processContent(final ClientRequest clientRequest, final ContentProvider entity) throws IOException { + if (entity == null) { + return; + } + + final OutputStreamContentProvider streamContentProvider = (OutputStreamContentProvider) entity; + try (final OutputStream output = streamContentProvider.getOutputStream()) { + clientRequest.writeEntity(); + } + } + + @Override + public Future<?> apply(final ClientRequest jerseyRequest, final AsyncConnectorCallback callback) { + final Request jettyRequest = translateRequest(jerseyRequest); + final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); + final ContentProvider entity = getStreamProvider(jerseyRequest); + if (entity != null) { + jettyRequest.content(entity); + } + final AtomicBoolean callbackInvoked = new AtomicBoolean(false); + final Throwable failure; + try { + final CompletableFuture<ClientResponse> responseFuture = + new CompletableFuture<ClientResponse>().whenComplete( + (clientResponse, throwable) -> { + if (throwable != null && throwable instanceof CancellationException) { + // take care of future cancellation + jettyRequest.abort(throwable); + + } + }); + + final AtomicReference<ClientResponse> jerseyResponse = new AtomicReference<>(); + final ByteBufferInputStream entityStream = new ByteBufferInputStream(); + jettyRequest.send(new Response.Listener.Adapter() { + + @Override + public void onHeaders(final Response jettyResponse) { + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(), + JettyConnector.this.getClass().getName()); + + if (responseFuture.isDone()) { + if (!callbackInvoked.compareAndSet(false, true)) { + return; + } + } + final ClientResponse response = translateResponse(jerseyRequest, jettyResponse, entityStream); + jerseyResponse.set(response); + } + + @Override + public void onContent(final Response jettyResponse, final ByteBuffer content) { + try { + // content must be consumed before returning from this method. + + if (content.hasArray()) { + byte[] array = content.array(); + byte[] buff = new byte[content.remaining()]; + System.arraycopy(array, content.arrayOffset(), buff, 0, content.remaining()); + entityStream.put(ByteBuffer.wrap(buff)); + } else { + byte[] buff = new byte[content.remaining()]; + content.get(buff); + entityStream.put(ByteBuffer.wrap(buff)); + } + } catch (final InterruptedException ex) { + final ProcessingException pe = new ProcessingException(ex); + entityStream.closeQueue(pe); + // try to complete the future with an exception + responseFuture.completeExceptionally(pe); + Thread.currentThread().interrupt(); + } + } + + @Override + public void onComplete(final Result result) { + entityStream.closeQueue(); + callback.response(jerseyResponse.get()); + responseFuture.complete(jerseyResponse.get()); + } + + @Override + public void onFailure(final Response response, final Throwable t) { + entityStream.closeQueue(t); + // try to complete the future with an exception + responseFuture.completeExceptionally(t); + if (callbackInvoked.compareAndSet(false, true)) { + callback.failure(t); + } + } + }); + processContent(jerseyRequest, entity); + return responseFuture; + } catch (final Throwable t) { + failure = t; + } + + if (callbackInvoked.compareAndSet(false, true)) { + callback.failure(failure); + } + CompletableFuture<Object> future = new CompletableFuture<>(); + future.completeExceptionally(failure); + return future; + } + + private static ClientResponse translateResponse(final ClientRequest jerseyRequest, + final org.eclipse.jetty.client.api.Response jettyResponse, + final NonBlockingInputStream entityStream) { + final ClientResponse jerseyResponse = new ClientResponse(Statuses.from(jettyResponse.getStatus()), jerseyRequest); + processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse); + jerseyResponse.setEntityStream(entityStream); + return jerseyResponse; + } + + @Override + public String getName() { + return "Jetty HttpClient " + Jetty.VERSION; + } + + @Override + public void close() { + try { + client.stop(); + } catch (final Exception e) { + throw new ProcessingException("Failed to stop the client.", e); + } + } +}
diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnectorProvider.java b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnectorProvider.java new file mode 100644 index 0000000..c2fb054 --- /dev/null +++ b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnectorProvider.java
@@ -0,0 +1,121 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configurable; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.client.Initializable; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +import org.eclipse.jetty.client.HttpClient; + +/** + * A {@link ConnectorProvider} for Jersey {@link Connector connector} + * instances that utilize the Jetty HTTP Client to send and receive + * HTTP request and responses. + * <p> + * The following connector configuration properties are supported: + * <ul> + * <li>{@link org.glassfish.jersey.client.ClientProperties#ASYNC_THREADPOOL_SIZE}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#CONNECT_TIMEOUT}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li> + * <li>{@link JettyClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li> + * <li>{@link JettyClientProperties#DISABLE_COOKIES}</li> + * </ul> + * </p> + * <p> + * This transport supports both synchronous and asynchronous processing of client requests. + * The following methods are supported: GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, CONNECT and MOVE. + * </p> + * <p> + * Typical usage: + * </p> + * <pre> + * {@code + * ClientConfig config = new ClientConfig(); + * config.connectorProvider(new JettyConnectorProvider()); + * Client client = ClientBuilder.newClient(config); + * + * // async request + * WebTarget target = client.target("http://localhost:8080"); + * Future<Response> future = target.path("resource").request().async().get(); + * + * // wait for 3 seconds + * Response response = future.get(3, TimeUnit.SECONDS); + * String entity = response.readEntity(String.class); + * client.close(); + * } + * </pre> + * <p> + * Connector instances created via Jetty HTTP Client-based connector provider support only + * {@link org.glassfish.jersey.client.RequestEntityProcessing#BUFFERED entity buffering}. + * Defining the property {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} has no + * effect on Jetty HTTP Client-based connectors. + * </p> + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.5 + */ +public class JettyConnectorProvider implements ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return new JettyConnector(client, runtimeConfig); + } + + /** + * Retrieve the underlying Jetty {@link org.eclipse.jetty.client.HttpClient} instance from + * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget} + * configured to use {@code JettyConnectorProvider}. + * + * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use + * {@code JettyConnectorProvider}. + * @return underlying Jetty {@code HttpClient} instance. + * + * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient} + * nor {@code JerseyWebTarget} instance or in case the component + * is not configured to use a {@code JettyConnectorProvider}. + * @since 2.8 + */ + public static HttpClient getHttpClient(Configurable<?> component) { + if (!(component instanceof Initializable)) { + throw new IllegalArgumentException( + LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName())); + } + + final Initializable<?> initializable = (Initializable<?>) component; + Connector connector = initializable.getConfiguration().getConnector(); + if (connector == null) { + initializable.preInitialize(); + connector = initializable.getConfiguration().getConnector(); + } + + if (connector instanceof JettyConnector) { + return ((JettyConnector) connector).getHttpClient(); + } + + throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED()); + } +}
diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/package-info.java b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/package-info.java new file mode 100644 index 0000000..3fb6ac1 --- /dev/null +++ b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/package-info.java
@@ -0,0 +1,21 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on the + * Jetty Client. + */ +package org.glassfish.jersey.jetty.connector;
diff --git a/connectors/jetty-connector/src/main/resources/org/glassfish/jersey/jetty/connector/localization.properties b/connectors/jetty-connector/src/main/resources/org/glassfish/jersey/jetty/connector/localization.properties new file mode 100644 index 0000000..15d9708 --- /dev/null +++ b/connectors/jetty-connector/src/main/resources/org/glassfish/jersey/jetty/connector/localization.properties
@@ -0,0 +1,22 @@ +# +# Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# {0} - HTTP method, e.g. GET, DELETE +method.not.supported=Method {0} not supported. +# {0} - property name - jersey.config.client.proxyUri +wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI. +invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget. +expected.connector.provider.not.used=The supplied component is not configured to use a JettyConnectorProvider.
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AsyncTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AsyncTest.java new file mode 100644 index 0000000..7d62881 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AsyncTest.java
@@ -0,0 +1,199 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.container.TimeoutHandler; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.hamcrest.Matchers; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Asynchronous connector test. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class AsyncTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName()); + private static final String PATH = "async"; + + /** + * Asynchronous test resource. + */ + @Path(PATH) + public static class AsyncResource { + /** + * Typical long-running operation duration. + */ + public static final long OPERATION_DURATION = 1000; + + /** + * Long-running asynchronous post. + * + * @param asyncResponse async response. + * @param id post request id (received as request payload). + */ + @POST + public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) { + LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName()); + new Thread(new Runnable() { + + @Override + public void run() { + String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep() + try { + Thread.sleep(OPERATION_DURATION); + return "DONE-" + id; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED-" + id; + } finally { + LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName()); + } + } + }, "async-post-runner-" + id).start(); + } + + /** + * Long-running async get request that times out. + * + * @param asyncResponse async response. + */ + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName()); + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + } + }); + asyncResponse.setTimeout(1, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // very expensive operation that typically finishes within 1 second but can take up to 5 seconds, + // simulated using sleep() + try { + Thread.sleep(5 * OPERATION_DURATION); + return "DONE"; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED"; + } finally { + LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName()); + } + } + }).start(); + } + + } + + @Override + protected Application configure() { + return new ResourceConfig(AsyncResource.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + } + + @Override + protected void configureClient(ClientConfig config) { + // TODO: fails with true on request - should be fixed by resolving JERSEY-2273 + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY)); + config.connectorProvider(new JettyConnectorProvider()); + } + + /** + * Test asynchronous POST. + * + * Send 3 async POST requests and wait to receive the responses. Check the response content and + * assert that the operation did not take more than twice as long as a single long operation duration + * (this ensures async request execution). + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncPost() throws Exception { + final long tic = System.currentTimeMillis(); + + // Submit requests asynchronously. + final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1")); + final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2")); + final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3")); + // get() waits for the response + final String r1 = rf1.get().readEntity(String.class); + final String r2 = rf2.get().readEntity(String.class); + final String r3 = rf3.get().readEntity(String.class); + + final long toc = System.currentTimeMillis(); + + assertEquals("DONE-1", r1); + assertEquals("DONE-2", r2); + assertEquals("DONE-3", r3); + + assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION)); + } + + /** + * Test accessing an operation that times out on the server. + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncGetWithTimeout() throws Exception { + final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthFilterTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthFilterTest.java new file mode 100644 index 0000000..a3c90f7 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthFilterTest.java
@@ -0,0 +1,77 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class AuthFilterTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(AuthFilterTest.class.getName()); + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(AuthTest.AuthResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testAuthGetWithClientFilter() { + client().register(HttpAuthenticationFeature.basic("name", "password")); + Response response = target("test/filter").request().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testAuthPostWithClientFilter() { + client().register(HttpAuthenticationFeature.basic("name", "password")); + Response response = target("test/filter").request().post(Entity.text("POST")); + assertEquals("POST", response.readEntity(String.class)); + } + + + @Test + public void testAuthDeleteWithClientFilter() { + client().register(HttpAuthenticationFeature.basic("name", "password")); + Response response = target("test/filter").request().delete(); + assertEquals(204, response.getStatus()); + } + +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthTest.java new file mode 100644 index 0000000..9008d9b --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthTest.java
@@ -0,0 +1,197 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import javax.inject.Singleton; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class AuthTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(AuthTest.class.getName()); + private static final String PATH = "test"; + + @Path("/test") + @Singleton + public static class AuthResource { + + int requestCount = 0; + + @GET + public String get(@Context HttpHeaders h) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + + return "GET"; + } + + @GET + @Path("filter") + public String getFilter(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return "GET"; + } + + @POST + public String post(@Context HttpHeaders h, String e) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + + return e; + } + + @POST + @Path("filter") + public String postFilter(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return e; + } + + @DELETE + public void delete(@Context HttpHeaders h) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + } + + @DELETE + @Path("filter") + public void deleteFilter(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + } + + @DELETE + @Path("filter/withEntity") + public String deleteFilterWithEntity(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return e; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(AuthResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Test + public void testAuthGet() { + ClientConfig config = new ClientConfig(); + config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new JettyConnectorProvider()); + Client client = ClientBuilder.newClient(config); + + Response response = client.target(getBaseUri()).path(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + client.close(); + } + + @Test + public void testAuthPost() { + ClientConfig config = new ClientConfig(); + config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new JettyConnectorProvider()); + Client client = ClientBuilder.newClient(config); + + Response response = client.target(getBaseUri()).path(PATH).request().post(Entity.text("POST")); + assertEquals("POST", response.readEntity(String.class)); + client.close(); + } + + @Test + public void testAuthDelete() { + ClientConfig config = new ClientConfig(); + config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new JettyConnectorProvider()); + Client client = ClientBuilder.newClient(config); + + Response response = client.target(getBaseUri()).path(PATH).request().delete(); + assertEquals(response.getStatus(), 204); + client.close(); + } + +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/CookieTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/CookieTest.java new file mode 100644 index 0000000..d6cb6f1 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/CookieTest.java
@@ -0,0 +1,121 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class CookieTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(CookieTest.class.getName()); + + @Path("/") + public static class CookieResource { + @GET + public Response get(@Context HttpHeaders h) { + Cookie c = h.getCookies().get("name"); + String e = (c == null) ? "NO-COOKIE" : c.getValue(); + return Response.ok(e) + .cookie(new NewCookie("name", "value")).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(CookieResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Test + public void testCookieResource() { + ClientConfig config = new ClientConfig(); + config.connectorProvider(new JettyConnectorProvider()); + Client client = ClientBuilder.newClient(config); + WebTarget r = client.target(getBaseUri()); + + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("value", r.request().get(String.class)); + client.close(); + } + + @Test + public void testDisabledCookies() { + ClientConfig cc = new ClientConfig(); + cc.property(JettyClientProperties.DISABLE_COOKIES, true); + cc.connectorProvider(new JettyConnectorProvider()); + JerseyClient client = JerseyClientBuilder.createClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("NO-COOKIE", r.request().get(String.class)); + + final JettyConnector connector = (JettyConnector) client.getConfiguration().getConnector(); + if (connector.getCookieStore() != null) { + assertTrue(connector.getCookieStore().getCookies().isEmpty()); + } else { + assertNull(connector.getCookieStore()); + } + client.close(); + } + + @Test + public void testCookies() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new JettyConnectorProvider()); + JerseyClient client = JerseyClientBuilder.createClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("value", r.request().get(String.class)); + + final JettyConnector connector = (JettyConnector) client.getConfiguration().getConnector(); + assertNotNull(connector.getCookieStore().getCookies()); + assertEquals(1, connector.getCookieStore().getCookies().size()); + assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue()); + client.close(); + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/CustomLoggingFilter.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/CustomLoggingFilter.java new file mode 100644 index 0000000..8ab646b --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/CustomLoggingFilter.java
@@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.io.IOException; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; + +import static org.junit.Assert.assertEquals; + +/** + * Custom logging filter. + * + * @author Santiago Pericas-Geertsen (santiago.pericasgeertsen at oracle.com) + */ +public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter, + ClientRequestFilter, ClientResponseFilter { + + static int preFilterCalled = 0; + static int postFilterCalled = 0; + + @Override + public void filter(ClientRequestContext context) throws IOException { + System.out.println("CustomLoggingFilter.preFilter called"); + assertEquals(context.getConfiguration().getProperty("foo"), "bar"); + preFilterCalled++; + } + + @Override + public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException { + System.out.println("CustomLoggingFilter.postFilter called"); + assertEquals(context.getConfiguration().getProperty("foo"), "bar"); + postFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context) throws IOException { + System.out.println("CustomLoggingFilter.preFilter called"); + assertEquals(context.getProperty("foo"), "bar"); + preFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException { + System.out.println("CustomLoggingFilter.postFilter called"); + assertEquals(context.getProperty("foo"), "bar"); + postFilterCalled++; + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/EntityTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/EntityTest.java new file mode 100644 index 0000000..eaaa126 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/EntityTest.java
@@ -0,0 +1,158 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Tests the Http content negotiation. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class EntityTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName()); + + private static final String PATH = "test"; + + @Path("/test") + public static class EntityResource { + + @GET + public Person get() { + return new Person("John", "Doe"); + } + + @POST + public Person post(Person entity) { + return entity; + } + + } + + @XmlRootElement + public static class Person { + + private String firstName; + private String lastName; + + public Person() { + } + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public String toString() { + return firstName + " " + lastName; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(EntityResource.class, JacksonFeature.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()) + .register(JacksonFeature.class); + } + + @Test + public void testGet() { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).get(); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).get(); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } + + @Test + public void testGetAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async().get().get(); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().get().get(); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } + + @Test + public void testPost() { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).post(Entity.xml(new Person("John", "Doe"))); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.xml(new Person("John", "Doe"))); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } + + @Test + public void testPostAsync() throws ExecutionException, InterruptedException, TimeoutException { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async() + .post(Entity.xml(new Person("John", "Doe"))).get(); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().post(Entity.xml(new Person("John", "Doe"))) + .get(); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/ErrorTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/ErrorTest.java new file mode 100644 index 0000000..3425f63 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/ErrorTest.java
@@ -0,0 +1,124 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class ErrorTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(ErrorTest.class.getName()); + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(ErrorResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + + @Path("/test") + public static class ErrorResource { + @POST + public Response post(String entity) { + return Response.serverError().build(); + } + + @Path("entity") + @POST + public Response postWithEntity(String entity) { + return Response.serverError().entity("error").build(); + } + } + + @Test + public void testPostError() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + } + } + } + + @Test + public void testPostErrorWithEntity() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + String s = ex.getResponse().readEntity(String.class); + assertEquals("error", s); + } + } + } + + @Test + public void testPostErrorAsync() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().async().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + } + } + } + + @Test + public void testPostErrorWithEntityAsync() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().async().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + String s = ex.getResponse().readEntity(String.class); + assertEquals("error", s); + } + } + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/FollowRedirectsTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/FollowRedirectsTest.java new file mode 100644 index 0000000..0edda7a --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/FollowRedirectsTest.java
@@ -0,0 +1,142 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Jetty connector follow redirect tests. + * + * @author Martin Matula + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class FollowRedirectsTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.class.getName()); + + @Path("/test") + public static class RedirectResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("redirect") + public Response redirect() { + return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(RedirectResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.FOLLOW_REDIRECTS, false); + config.connectorProvider(new JettyConnectorProvider()); + } + + private static class RedirectTestFilter implements ClientResponseFilter { + public static final String RESOLVED_URI_HEADER = "resolved-uri"; + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (responseContext instanceof ClientResponse) { + ClientResponse clientResponse = (ClientResponse) responseContext; + responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString()); + } + } + } + + @Test + public void testDoFollow() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new JettyConnectorProvider()); + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + Response r = t.path("test/redirect") + .register(RedirectTestFilter.class) + .request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); +// TODO uncomment as part of JERSEY-2388 fix. +// assertEquals( +// UriBuilder.fromUri(getBaseUri()).path(RedirectResource.class).build().toString(), +// r.getHeaderString(RedirectTestFilter.RESOLVED_URI_HEADER)); + + c.close(); + } + + @Test + public void testDoFollowPerRequestOverride() { + WebTarget t = target("test/redirect"); + t.property(ClientProperties.FOLLOW_REDIRECTS, true); + Response r = t.request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testDontFollow() { + WebTarget t = target("test/redirect"); + assertEquals(303, t.request().get().getStatus()); + } + + @Test + public void testDontFollowPerRequestOverride() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new JettyConnectorProvider()); + Client client = ClientBuilder.newClient(config); + WebTarget t = client.target(u); + t.property(ClientProperties.FOLLOW_REDIRECTS, false); + Response r = t.path("test/redirect").request().get(); + assertEquals(303, r.getStatus()); + client.close(); + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/GZIPContentEncodingTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/GZIPContentEncodingTest.java new file mode 100644 index 0000000..9ecb0aa --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,105 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.Arrays; +import java.util.logging.Logger; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.message.GZipEncoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class GZIPContentEncodingTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName()); + + @Path("/") + public static class Resource { + + @POST + public byte[] post(byte[] content) { + return content; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(Resource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(GZipEncoder.class); + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testPost() { + WebTarget r = target(); + byte[] content = new byte[1024 * 1024]; + assertTrue(Arrays.equals(content, + r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class))); + + Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + ClientConfig config = new ClientConfig(); + config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024); + config.connectorProvider(new JettyConnectorProvider()); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + + Client client = ClientBuilder.newClient(config); + WebTarget r = client.target(getBaseUri()); + + byte[] content = new byte[1024 * 1024]; + assertTrue(Arrays.equals(content, + r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class))); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + + client.close(); + } + +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/HelloWorldTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/HelloWorldTest.java new file mode 100644 index 0000000..9c70675 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/HelloWorldTest.java
@@ -0,0 +1,227 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class HelloWorldTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName()); + private static final String ROOT_PATH = "helloworld"; + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HelloWorldResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.ASYNC_THREADPOOL_SIZE, 20); + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testConnection() { + Response response = target().path(ROOT_PATH).request("text/plain").get(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testClientStringResponse() { + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + } + + @Test + public void testAsyncClientRequests() throws InterruptedException { + final int REQUESTS = 20; + final CountDownLatch latch = new CountDownLatch(REQUESTS); + final long tic = System.currentTimeMillis(); + for (int i = 0; i < REQUESTS; i++) { + final int id = i; + target().path(ROOT_PATH).request().async().get(new InvocationCallback<Response>() { + @Override + public void completed(Response response) { + try { + final String result = response.readEntity(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, result); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(Throwable error) { + error.printStackTrace(); + latch.countDown(); + } + }); + } + latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS); + final long toc = System.currentTimeMillis(); + Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic)); + } + + @Test + public void testHead() { + Response response = target().path(ROOT_PATH).request().head(); + assertEquals(200, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + } + + @Test + public void testFooBarOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals("foo/bar", response.getMediaType().toString()); + assertEquals(0, response.getLength()); + } + + @Test + public void testTextPlainOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + final String responseBody = response.readEntity(String.class); + _checkAllowContent(responseBody); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testMissingResourceNotFound() { + Response response; + + response = target().path(ROOT_PATH + "arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + + response = target().path(ROOT_PATH).path("arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + } + + @Test + public void testLoggingFilterClientClass() { + Client client = client(); + client.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + + @Test + public void testLoggingFilterClientInstance() { + Client client = client(); + client.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + + @Test + public void testLoggingFilterTargetClass() { + WebTarget target = target().path(ROOT_PATH); + target.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetInstance() { + WebTarget target = target().path(ROOT_PATH); + target.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testConfigurationUpdate() { + Client client1 = client(); + client1.register(CustomLoggingFilter.class).property("foo", "bar"); + + Client client = ClientBuilder.newClient(client1.getConfiguration()); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/HttpHeadersTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/HttpHeadersTest.java new file mode 100644 index 0000000..b6e3ec8 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/HttpHeadersTest.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.List; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +/** + * Tests the headers. + * + * @author Stepan Kopriva + */ +public class HttpHeadersTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + return "POST"; + } + + @GET + public String testUserAgent(@Context HttpHeaders httpHeaders) { + final List<String> requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT); + if (requestHeader.size() != 1) { + return "FAIL"; + } + return requestHeader.get(0); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testPost() { + Response response = target().path("test").request().header("X-CLIENT", "client").post(null); + + assertEquals(200, response.getStatus()); + assertTrue(response.hasEntity()); + } + + /** + * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client. + */ + @Test + public void testUserAgent() { + String response = target().path("test").request().get(String.class); + assertTrue("User-agent header should start with 'Jersey', but was " + response, response.startsWith("Jersey")); + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/ManagedClientTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/ManagedClientTest.java new file mode 100644 index 0000000..fb1ce62 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/ManagedClientTest.java
@@ -0,0 +1,256 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ClientBinding; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.Uri; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Jersey programmatic managed client test + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ManagedClientTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName()); + + /** + * Managed client configuration for client A. + */ + @ClientBinding(configClass = MyClientAConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public static @interface ClientA { + } + + /** + * Managed client configuration for client B. + */ + @ClientBinding(configClass = MyClientBConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface ClientB { + } + + /** + * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance + * to every method that is annotated with {@link Require @Require} internal feature + * annotation. + */ + public static class CustomHeaderFeature implements DynamicFeature { + + /** + * A method annotation to be placed on those resource methods to which a validating + * {@link CustomHeaderFilter} instance should be added. + */ + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target(ElementType.METHOD) + public static @interface Require { + + /** + * Expected custom header name to be validated by the {@link CustomHeaderFilter}. + */ + public String headerName(); + + /** + * Expected custom header value to be validated by the {@link CustomHeaderFilter}. + */ + public String headerValue(); + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class); + if (va != null) { + context.register(new CustomHeaderFilter(va.headerName(), va.headerValue())); + } + } + } + + /** + * A filter for appending and validating custom headers. + * <p> + * On the client side, appends a new custom request header with a configured name and value to each outgoing request. + * </p> + * <p> + * On the server side, validates that each request has a custom header with a configured name and value. + * If the validation fails a HTTP 403 response is returned. + * </p> + */ + public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter { + + private final String headerName; + private final String headerValue; + + public CustomHeaderFilter(String headerName, String headerValue) { + if (headerName == null || headerValue == null) { + throw new IllegalArgumentException("Header name and value must not be null."); + } + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public void filter(ContainerRequestContext ctx) throws IOException { // validate + if (!headerValue.equals(ctx.getHeaderString(headerName))) { + ctx.abortWith(Response.status(Response.Status.FORBIDDEN) + .type(MediaType.TEXT_PLAIN) + .entity(String + .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue)) + .build()); + } + } + + @Override + public void filter(ClientRequestContext ctx) throws IOException { // append + ctx.getHeaders().putSingle(headerName, headerValue); + } + } + + /** + * Internal resource accessed from the managed client resource. + */ + @Path("internal") + public static class InternalResource { + + @GET + @Path("a") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a") + public String getA() { + return "a"; + } + + @GET + @Path("b") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b") + public String getB() { + return "b"; + } + } + + /** + * A resource that uses managed clients to retrieve values of internal + * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter} + * and require a specific custom header in a request to be set to a specific value. + * <p> + * Properly configured managed clients have a {@code CustomHeaderFilter} instance + * configured to insert the {@link CustomHeaderFeature.Require required} custom header + * with a proper value into the outgoing client requests. + * </p> + */ + @Path("public") + public static class PublicResource { + + @Uri("a") + @ClientA // resolves to <base>/internal/a + private WebTarget targetA; + + @GET + @Produces("text/plain") + @Path("a") + public String getTargetA() { + return targetA.request(MediaType.TEXT_PLAIN).get(String.class); + } + + @GET + @Produces("text/plain") + @Path("b") + public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) { + return targetB.request(MediaType.TEXT_PLAIN).get(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class) + .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal"); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + public static class MyClientAConfig extends ClientConfig { + + public MyClientAConfig() { + this.register(new CustomHeaderFilter("custom-header", "a")); + } + } + + public static class MyClientBConfig extends ClientConfig { + + public MyClientBConfig() { + this.register(new CustomHeaderFilter("custom-header", "b")); + } + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + /** + * Test that a connection via managed clients works properly. + * + * @throws Exception in case of test failure. + */ + @Test + public void testManagedClient() throws Exception { + final WebTarget resource = target().path("public").path("{name}"); + Response response; + + response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("a", response.readEntity(String.class)); + + response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("b", response.readEntity(String.class)); + } + +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/MethodTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/MethodTest.java new file mode 100644 index 0000000..78393eb --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/MethodTest.java
@@ -0,0 +1,146 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.PATCH; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Tests the Http methods. + * + * @author Stepan Kopriva + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class MethodTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(MethodTest.class.getName()); + + private static final String PATH = "test"; + + @Path("/test") + public static class HttpMethodResource { + @GET + public String get() { + return "GET"; + } + + @POST + public String post(String entity) { + return entity; + } + + @PUT + public String put(String entity) { + return entity; + } + + @PATCH + public String patch(String entity) { + return entity; + } + + @DELETE + public String delete() { + return "DELETE"; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testGet() { + Response response = target(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testGetAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().get().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testPost() { + Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN)); + assertEquals("POST", response.readEntity(String.class)); + } + + @Test + public void testPostAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().post(Entity.entity("POST", MediaType.TEXT_PLAIN)).get(); + assertEquals("POST", response.readEntity(String.class)); + } + + @Test + public void testPut() { + Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)); + assertEquals("PUT", response.readEntity(String.class)); + } + + @Test + public void testPutAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)).get(); + assertEquals("PUT", response.readEntity(String.class)); + } + + @Test + public void testDelete() { + Response response = target(PATH).request().delete(); + assertEquals("DELETE", response.readEntity(String.class)); + } + + @Test + public void testDeleteAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().delete().get(); + assertEquals("DELETE", response.readEntity(String.class)); + } + + @Test + public void testPatch() { + Response response = target(PATH).request().method("PATCH", Entity.entity("PATCH", MediaType.TEXT_PLAIN)); + assertEquals("PATCH", response.readEntity(String.class)); + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/NoEntityTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/NoEntityTest.java new file mode 100644 index 0000000..35ca436 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/NoEntityTest.java
@@ -0,0 +1,102 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class NoEntityTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @GET + public Response get() { + return Response.status(Status.CONFLICT).build(); + } + + @POST + public void post(String entity) { + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testGet() { + WebTarget r = target("test"); + + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testGetWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testPost() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + } + } + + @Test + public void testPostWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + cr.close(); + } + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java new file mode 100644 index 0000000..d29244f --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.net.URI; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Martin Matula + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class TimeoutTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName()); + + @Path("/test") + public static class TimeoutResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("timeout") + public String getTimeout() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "GET"; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TimeoutResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyConnectorProvider()); + } + + @Test + public void testFast() { + Response r = target("test").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testSlow() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1000); + config.connectorProvider(new JettyConnectorProvider()); + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + try { + t.path("test/timeout").request().get(); + fail("Timeout expected."); + } catch (ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(TimeoutException.class)); + } finally { + c.close(); + } + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TraceSupportTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TraceSupportTest.java new file mode 100644 index 0000000..87cac21 --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TraceSupportTest.java
@@ -0,0 +1,235 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This very basic resource showcases support of a HTTP TRACE method, + * not directly supported by JAX-RS API. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class TraceSupportTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName()); + + /** + * Programmatic tracing root resource path. + */ + public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic"; + + /** + * Annotated class-based tracing root resource path. + */ + public static final String ROOT_PATH_ANNOTATED = "tracing/annotated"; + + @HttpMethod(TRACE.NAME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TRACE { + public static final String NAME = "TRACE"; + } + + @Path(ROOT_PATH_ANNOTATED) + public static class TracingResource { + + @TRACE + @Produces("text/plain") + public String trace(Request request) { + return stringify((ContainerRequest) request); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TracingResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC); + resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector<ContainerRequestContext, Response>() { + + @Override + public Response apply(ContainerRequestContext request) { + if (request == null) { + return Response.noContent().build(); + } else { + return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build(); + } + } + }); + + return config.registerResources(resourceBuilder.build()); + + } + + private String[] expectedFragmentsProgrammatic = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic" + }; + private String[] expectedFragmentsAnnotated = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/annotated" + }; + + private WebTarget prepareTarget(String path) { + final WebTarget target = target(); + target.register(LoggingFeature.class); + return target.path(path); + } + + @Test + public void testProgrammaticApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsProgrammatic) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testAnnotatedApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsAnnotated) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testTraceWithEntity() throws Exception { + _testTraceWithEntity(false, false); + } + + @Test + public void testAsyncTraceWithEntity() throws Exception { + _testTraceWithEntity(true, false); + } + + @Test + public void testTraceWithEntityJettyConnector() throws Exception { + _testTraceWithEntity(false, true); + } + + @Test + public void testAsyncTraceWithEntityJettyConnector() throws Exception { + _testTraceWithEntity(true, true); + } + + private void _testTraceWithEntity(final boolean isAsync, final boolean useJettyConnection) throws Exception { + try { + WebTarget target = useJettyConnection ? getJettyClient().target(target().getUri()) : target(); + target = target.path(ROOT_PATH_ANNOTATED); + + final Entity<String> entity = Entity.entity("trace", MediaType.WILDCARD_TYPE); + + Response response; + if (!isAsync) { + response = target.request().method(TRACE.NAME, entity); + } else { + response = target.request().async().method(TRACE.NAME, entity).get(); + } + + fail("A TRACE request MUST NOT include an entity. (response=" + response + ")"); + } catch (Exception e) { + // OK + } + } + + private Client getJettyClient() { + return ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyConnectorProvider())); + } + + + public static String stringify(ContainerRequest request) { + StringBuilder buffer = new StringBuilder(); + + printRequestLine(buffer, request); + printPrefixedHeaders(buffer, request.getHeaders()); + + if (request.hasEntity()) { + buffer.append(request.readEntity(String.class)).append("\n"); + } + + return buffer.toString(); + } + + private static void printRequestLine(StringBuilder buffer, ContainerRequest request) { + buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n"); + } + + private static void printPrefixedHeaders(StringBuilder buffer, Map<String, List<String>> headers) { + for (Map.Entry<String, List<String>> e : headers.entrySet()) { + List<String> val = e.getValue(); + String header = e.getKey(); + + if (val.size() == 1) { + buffer.append(header).append(": ").append(val.get(0)).append("\n"); + } else { + StringBuilder sb = new StringBuilder(); + boolean add = false; + for (String s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + buffer.append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } +}
diff --git a/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/UnderlyingHttpClientAccessTest.java new file mode 100644 index 0000000..7cefc2b --- /dev/null +++ b/connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.jersey.client.ClientConfig; + +import org.eclipse.jetty.client.HttpClient; +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * Test of access to the underlying HTTP client instance used by the connector. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class UnderlyingHttpClientAccessTest { + + /** + * Verifier of JERSEY-2424 fix. + */ + @Test + public void testHttpClientInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyConnectorProvider())); + final HttpClient hcOnClient = JettyConnectorProvider.getHttpClient(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getHttpClient method above. + final WebTarget target = client.target("http://localhost/"); + final HttpClient hcOnTarget = JettyConnectorProvider.getHttpClient(target); + + assertNotNull("HTTP client instance set on JerseyClient should not be null.", hcOnClient); + assertNotNull("HTTP client instance set on JerseyWebTarget should not be null.", hcOnTarget); + assertSame("HTTP client instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget" + + "(provided the target instance has not been further configured).", + hcOnClient, hcOnTarget + ); + } +}
diff --git a/connectors/netty-connector/pom.xml b/connectors/netty-connector/pom.xml new file mode 100644 index 0000000..dd6fc6a --- /dev/null +++ b/connectors/netty-connector/pom.xml
@@ -0,0 +1,78 @@ +<?xml version="1.0"?> +<!-- + + Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-netty-connector</artifactId> + <packaging>jar</packaging> + <name>jersey-connectors-netty</name> + + <description>Jersey Client Transport via Netty</description> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + </dependency> + + <dependency> + <!-- cannot be netty, since netty container depends on netty connector. --> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-grizzly2</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project>
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java new file mode 100644 index 0000000..51e78e8 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -0,0 +1,158 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingDeque; + +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.netty.connector.internal.NettyInputStream; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; + +/** + * Jersey implementation of Netty channel handler. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> { + + private final NettyConnector connector; + private final LinkedBlockingDeque<InputStream> isList = new LinkedBlockingDeque<>(); + + private final AsyncConnectorCallback asyncConnectorCallback; + private final ClientRequest jerseyRequest; + private final CompletableFuture future; + + JerseyClientHandler(NettyConnector nettyConnector, ClientRequest request, + AsyncConnectorCallback callback, CompletableFuture future) { + this.connector = nettyConnector; + this.asyncConnectorCallback = callback; + this.jerseyRequest = request; + this.future = future; + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof HttpResponse) { + final HttpResponse response = (HttpResponse) msg; + + final ClientResponse jerseyResponse = new ClientResponse(new Response.StatusType() { + @Override + public int getStatusCode() { + return response.status().code(); + } + + @Override + public Response.Status.Family getFamily() { + return Response.Status.Family.familyOf(response.status().code()); + } + + @Override + public String getReasonPhrase() { + return response.status().reasonPhrase(); + } + }, jerseyRequest); + + for (Map.Entry<String, String> entry : response.headers().entries()) { + jerseyResponse.getHeaders().add(entry.getKey(), entry.getValue()); + } + + // request entity handling. + if ((response.headers().contains(HttpHeaderNames.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0) + || HttpUtil.isTransferEncodingChunked(response)) { + + ctx.channel().closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + isList.add(NettyInputStream.END_OF_INPUT_ERROR); + } + }); + + jerseyResponse.setEntityStream(new NettyInputStream(isList)); + } else { + jerseyResponse.setEntityStream(new InputStream() { + @Override + public int read() throws IOException { + return -1; + } + }); + } + + if (asyncConnectorCallback != null) { + connector.executorService.execute(new Runnable() { + @Override + public void run() { + asyncConnectorCallback.response(jerseyResponse); + future.complete(jerseyResponse); + } + }); + } + + } + if (msg instanceof HttpContent) { + + HttpContent httpContent = (HttpContent) msg; + + ByteBuf content = httpContent.content(); + + if (content.isReadable()) { + // copy bytes - when netty reads last chunk, it automatically closes the channel, which invalidates all + // relates ByteBuffs. + byte[] bytes = new byte[content.readableBytes()]; + content.getBytes(content.readerIndex(), bytes); + isList.add(new ByteArrayInputStream(bytes)); + } + + if (msg instanceof LastHttpContent) { + isList.add(NettyInputStream.END_OF_INPUT); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) { + if (asyncConnectorCallback != null) { + connector.executorService.execute(new Runnable() { + @Override + public void run() { + asyncConnectorCallback.failure(cause); + } + }); + } + future.completeExceptionally(cause); + isList.add(NettyInputStream.END_OF_INPUT_ERROR); + } +}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java new file mode 100644 index 0000000..d67c547 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -0,0 +1,306 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpChunkedInput; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.JdkSslContext; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.util.concurrent.GenericFutureListener; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.netty.connector.internal.JerseyChunkedInput; + +/** + * Netty connector implementation. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class NettyConnector implements Connector { + + final ExecutorService executorService; + final EventLoopGroup group; + final Client client; + + NettyConnector(Client client) { + + final Object threadPoolSize = client.getConfiguration().getProperties().get(ClientProperties.ASYNC_THREADPOOL_SIZE); + + if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { + executorService = Executors.newFixedThreadPool((Integer) threadPoolSize); + } else { + executorService = Executors.newCachedThreadPool(); + } + + this.group = new NioEventLoopGroup(); + this.client = client; + } + + @Override + public ClientResponse apply(ClientRequest jerseyRequest) { + + final AtomicReference<ClientResponse> syncResponse = new AtomicReference<>(null); + final AtomicReference<Throwable> syncException = new AtomicReference<>(null); + + try { + Future<?> resultFuture = apply(jerseyRequest, new AsyncConnectorCallback() { + @Override + public void response(ClientResponse response) { + syncResponse.set(response); + } + + @Override + public void failure(Throwable failure) { + syncException.set(failure); + } + }); + + Integer timeout = ClientProperties.getValue(jerseyRequest.getConfiguration().getProperties(), + ClientProperties.READ_TIMEOUT, 0); + + if (timeout != null && timeout > 0) { + resultFuture.get(timeout, TimeUnit.MILLISECONDS); + } else { + resultFuture.get(); + } + } catch (ExecutionException ex) { + Throwable e = ex.getCause() == null ? ex : ex.getCause(); + throw new ProcessingException(e.getMessage(), e); + } catch (Exception ex) { + throw new ProcessingException(ex.getMessage(), ex); + } + + Throwable throwable = syncException.get(); + if (throwable == null) { + return syncResponse.get(); + } else { + throw new RuntimeException(throwable); + } + } + + @Override + public Future<?> apply(final ClientRequest jerseyRequest, final AsyncConnectorCallback jerseyCallback) { + + final CompletableFuture<Object> settableFuture = new CompletableFuture<>(); + + final URI requestUri = jerseyRequest.getUri(); + String host = requestUri.getHost(); + int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80; + + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + + // Enable HTTPS if necessary. + if ("https".equals(requestUri.getScheme())) { + // making client authentication optional for now; it could be extracted to configurable property + JdkSslContext jdkSslContext = new JdkSslContext(client.getSslContext(), true, ClientAuth.NONE); + p.addLast(jdkSslContext.newHandler(ch.alloc())); + } + + // http proxy + Configuration config = jerseyRequest.getConfiguration(); + final Object proxyUri = config.getProperties().get(ClientProperties.PROXY_URI); + if (proxyUri != null) { + final URI u = getProxyUri(proxyUri); + + final String userName = ClientProperties.getValue( + config.getProperties(), ClientProperties.PROXY_USERNAME, String.class); + final String password = ClientProperties.getValue( + config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class); + + p.addLast(new HttpProxyHandler(new InetSocketAddress(u.getHost(), + u.getPort() == -1 ? 8080 : u.getPort()), + userName, password)); + } + + p.addLast(new HttpClientCodec()); + p.addLast(new ChunkedWriteHandler()); + p.addLast(new HttpContentDecompressor()); + p.addLast(new JerseyClientHandler(NettyConnector.this, jerseyRequest, jerseyCallback, settableFuture)); + } + }); + + // connect timeout + Integer connectTimeout = ClientProperties.getValue(jerseyRequest.getConfiguration().getProperties(), + ClientProperties.CONNECT_TIMEOUT, 0); + if (connectTimeout > 0) { + b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout); + } + + // Make the connection attempt. + final Channel ch = b.connect(host, port).sync().channel(); + + // guard against prematurely closed channel + final GenericFutureListener<io.netty.util.concurrent.Future<? super Void>> closeListener = + new GenericFutureListener<io.netty.util.concurrent.Future<? super Void>>() { + @Override + public void operationComplete(io.netty.util.concurrent.Future<? super Void> future) throws Exception { + if (!settableFuture.isDone()) { + settableFuture.completeExceptionally(new IOException("Channel closed.")); + } + } + }; + + ch.closeFuture().addListener(closeListener); + + HttpRequest nettyRequest; + + if (jerseyRequest.hasEntity()) { + nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.valueOf(jerseyRequest.getMethod()), + requestUri.getRawPath()); + } else { + nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.valueOf(jerseyRequest.getMethod()), + requestUri.getRawPath()); + } + + // headers + for (final Map.Entry<String, List<String>> e : jerseyRequest.getStringHeaders().entrySet()) { + nettyRequest.headers().add(e.getKey(), e.getValue()); + } + + // host header - http 1.1 + nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); + + if (jerseyRequest.hasEntity()) { + if (jerseyRequest.getLengthLong() == -1) { + HttpUtil.setTransferEncodingChunked(nettyRequest, true); + } else { + nettyRequest.headers().add(HttpHeaderNames.CONTENT_LENGTH, jerseyRequest.getLengthLong()); + } + } + + if (jerseyRequest.hasEntity()) { + // Send the HTTP request. + ch.writeAndFlush(nettyRequest); + + final JerseyChunkedInput jerseyChunkedInput = new JerseyChunkedInput(ch); + jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { + @Override + public OutputStream getOutputStream(int contentLength) throws IOException { + return jerseyChunkedInput; + } + }); + + if (HttpUtil.isTransferEncodingChunked(nettyRequest)) { + ch.write(new HttpChunkedInput(jerseyChunkedInput)); + } else { + ch.write(jerseyChunkedInput); + } + + executorService.execute(new Runnable() { + @Override + public void run() { + // close listener is not needed any more. + ch.closeFuture().removeListener(closeListener); + + try { + jerseyRequest.writeEntity(); + } catch (IOException e) { + jerseyCallback.failure(e); + settableFuture.completeExceptionally(e); + } + } + }); + + ch.flush(); + } else { + // close listener is not needed any more. + ch.closeFuture().removeListener(closeListener); + + // Send the HTTP request. + ch.writeAndFlush(nettyRequest); + } + + } catch (InterruptedException e) { + settableFuture.completeExceptionally(e); + return settableFuture; + } + + return settableFuture; + } + + @Override + public String getName() { + return "Netty 4.1.x"; + } + + @Override + public void close() { + group.shutdownGracefully(); + executorService.shutdown(); + } + + @SuppressWarnings("ChainOfInstanceofChecks") + private static URI getProxyUri(final Object proxy) { + if (proxy instanceof URI) { + return (URI) proxy; + } else if (proxy instanceof String) { + return URI.create((String) proxy); + } else { + throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI)); + } + } +}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java new file mode 100644 index 0000000..4716f67 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.Beta; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +/** + * Netty provider for Jersey {@link Connector connectors}. + * <p> + * The following connector configuration properties are supported: + * <ul> + * <li>{@link org.glassfish.jersey.client.ClientProperties#CONNECT_TIMEOUT}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li> + * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li> + * </ul> + * </p> + * <p> + * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an entity is not read from the response then + * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called after processing the response to release + * connection-based resources. + * </p> + * <p> + * If a response entity is obtained that is an instance of {@link java.io.Closeable} then the instance MUST + * be closed after processing the entity to release connection-based resources. + * <p/> + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @since 2.24 + */ +@Beta +public class NettyConnectorProvider implements ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return new NettyConnector(client); + } +}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java new file mode 100644 index 0000000..3ea9693 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
@@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import javax.inject.Provider; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.stream.ChunkedInput; + +/** + * Netty {@link ChunkedInput} implementation which also serves as an output + * stream to Jersey {@link javax.ws.rs.container.ContainerResponseContext}. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class JerseyChunkedInput extends OutputStream implements ChunkedInput<ByteBuf>, ChannelFutureListener { + + private static final ByteBuffer VOID = ByteBuffer.allocate(0); + private static final int CAPACITY = 8; + // TODO this needs to be configurable, see JERSEY-3228 + private static final int WRITE_TIMEOUT = 10000; + private static final int READ_TIMEOUT = 10000; + + private final LinkedBlockingDeque<ByteBuffer> queue = new LinkedBlockingDeque<>(CAPACITY); + private final Channel ctx; + private final ChannelFuture future; + + private volatile boolean open = true; + private volatile long offset = 0; + + public JerseyChunkedInput(Channel ctx) { + this.ctx = ctx; + this.future = ctx.closeFuture(); + this.future.addListener(this); + } + + @Override + public boolean isEndOfInput() throws Exception { + if (!open) { + return true; + } + + ByteBuffer peek = queue.peek(); + + if ((peek != null && peek == VOID)) { + queue.remove(); // VOID from the top. + open = false; + removeCloseListener(); + return true; + } + + return false; + } + + @Override + public void operationComplete(ChannelFuture f) throws Exception { + // forcibly closed connection. + open = false; + queue.clear(); + + close(); + removeCloseListener(); + } + + private void removeCloseListener() { + if (future != null) { + future.removeListener(this); + } + } + + @Override + @Deprecated + public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception { + return readChunk(ctx.alloc()); + } + + @Override + public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception { + + if (!open) { + return null; + } + + ByteBuffer top = queue.poll(READ_TIMEOUT, TimeUnit.MILLISECONDS); + + if (top == null) { + // returning empty buffer instead of null causes flush (which is needed for BroadcasterTest and others..). + return Unpooled.EMPTY_BUFFER; + } + + if (top == VOID) { + open = false; + return null; + } + + int topRemaining = top.remaining(); + ByteBuf buffer = allocator.buffer(topRemaining); + + buffer.setBytes(0, top); + buffer.setIndex(0, topRemaining); + + if (top.remaining() > 0) { + queue.addFirst(top); + } + + offset += topRemaining; + + return buffer; + } + + @Override + public long length() { + return -1; + } + + @Override + public long progress() { + return offset; + } + + @Override + public void close() throws IOException { + + if (queue.size() == CAPACITY) { + boolean offer = false; + + try { + offer = queue.offer(VOID, WRITE_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // ignore. + } + + if (!offer) { + queue.removeLast(); + queue.add(VOID); + } + } else { + queue.add(VOID); + } + + ctx.flush(); + } + + @Override + public void write(final int b) throws IOException { + + write(new Provider<ByteBuffer>() { + @Override + public ByteBuffer get() { + return ByteBuffer.wrap(new byte[]{(byte) b}); + } + }); + } + + @Override + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + + final byte[] bytes = new byte[len]; + System.arraycopy(b, off, bytes, 0, len); + + write(new Provider<ByteBuffer>() { + @Override + public ByteBuffer get() { + return ByteBuffer.wrap(bytes); + } + }); + } + + @Override + public void flush() throws IOException { + ctx.flush(); + } + + private void write(Provider<ByteBuffer> bufferSupplier) throws IOException { + + checkClosed(); + + try { + boolean queued = queue.offer(bufferSupplier.get(), WRITE_TIMEOUT, TimeUnit.MILLISECONDS); + if (!queued) { + throw new IOException("Buffer overflow."); + } + + } catch (InterruptedException e) { + throw new IOException(e); + } + } + + private void checkClosed() throws IOException { + if (!open) { + throw new IOException("Stream already closed."); + } + } +}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyInputStream.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyInputStream.java new file mode 100644 index 0000000..8ff47d6 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyInputStream.java
@@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * Input stream which servers as Request entity input. + * <p> + * Converts Netty NIO buffers to an input streams and stores them in the queue, + * waiting for Jersey to process it. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class NettyInputStream extends InputStream { + + private volatile boolean end = false; + + /** + * End of input. + */ + public static final InputStream END_OF_INPUT = new InputStream() { + @Override + public int read() throws IOException { + return 0; + } + + @Override + public String toString() { + return "END_OF_INPUT " + super.toString(); + } + }; + + /** + * Unexpected end of input. + */ + public static final InputStream END_OF_INPUT_ERROR = new InputStream() { + @Override + public int read() throws IOException { + return 0; + } + + @Override + public String toString() { + return "END_OF_INPUT_ERROR " + super.toString(); + } + }; + + private final LinkedBlockingDeque<InputStream> isList; + + public NettyInputStream(LinkedBlockingDeque<InputStream> isList) { + this.isList = isList; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + + if (end) { + return -1; + } + + InputStream take; + try { + take = isList.take(); + + if (checkEndOfInput(take)) { + return -1; + } + + int read = take.read(b, off, len); + + if (take.available() > 0) { + isList.addFirst(take); + } + + return read; + } catch (InterruptedException e) { + throw new IOException("Interrupted.", e); + } + } + + @Override + public int read() throws IOException { + + if (end) { + return -1; + } + + try { + InputStream take = isList.take(); + + if (checkEndOfInput(take)) { + return -1; + } + + int read = take.read(); + + if (take.available() > 0) { + isList.addFirst(take); + } + + return read; + } catch (InterruptedException e) { + throw new IOException("Interrupted.", e); + } + } + + @Override + public int available() throws IOException { + InputStream peek = isList.peek(); + if (peek != null) { + return peek.available(); + } + + return 0; + } + + private boolean checkEndOfInput(InputStream take) throws IOException { + if (take == END_OF_INPUT) { + end = true; + return true; + } else if (take == END_OF_INPUT_ERROR) { + end = true; + throw new IOException("Connection was closed prematurely."); + } + return false; + } +}
diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties new file mode 100644 index 0000000..2403307 --- /dev/null +++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
@@ -0,0 +1,17 @@ +# +# Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI.
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/AsyncTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/AsyncTest.java new file mode 100644 index 0000000..3237784 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/AsyncTest.java
@@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.container.TimeoutHandler; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.hamcrest.Matchers; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Asynchronous connector test. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class AsyncTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName()); + private static final String PATH = "async"; + + /** + * Asynchronous test resource. + */ + @Path(PATH) + public static class AsyncResource { + + /** + * Typical long-running operation duration. + */ + public static final long OPERATION_DURATION = 1000; + + /** + * Long-running asynchronous post. + * + * @param asyncResponse async response. + * @param id post request id (received as request payload). + */ + @POST + public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) { + LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName()); + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep() + try { + Thread.sleep(OPERATION_DURATION); + return "DONE-" + id; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED-" + id; + } finally { + LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName()); + } + } + }, "async-post-runner-" + id).start(); + } + + /** + * Long-running async get request that times out. + * + * @param asyncResponse async response. + */ + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName()); + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(final AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + } + }); + asyncResponse.setTimeout(1, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // very expensive operation that typically finishes within 1 second but can take up to 5 seconds, + // simulated using sleep() + try { + Thread.sleep(5 * OPERATION_DURATION); + return "DONE"; + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED"; + } finally { + LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName()); + } + } + }).start(); + } + + } + + @Override + protected Application configure() { + return new ResourceConfig(AsyncResource.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + } + + @Override + protected void configureClient(final ClientConfig config) { + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + config.connectorProvider(new NettyConnectorProvider()); + } + + /** + * Test asynchronous POST. + * <p/> + * Send 3 async POST requests and wait to receive the responses. Check the response content and + * assert that the operation did not take more than twice as long as a single long operation duration + * (this ensures async request execution). + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncPost() throws Exception { + final long tic = System.currentTimeMillis(); + + // Submit requests asynchronously. + final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1")); + final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2")); + final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3")); + // get() waits for the response + final String r1 = rf1.get().readEntity(String.class); + final String r2 = rf2.get().readEntity(String.class); + final String r3 = rf3.get().readEntity(String.class); + + final long toc = System.currentTimeMillis(); + + assertEquals("DONE-1", r1); + assertEquals("DONE-2", r2); + assertEquals("DONE-3", r3); + + assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION)); + } + + /** + * Test accessing an operation that times out on the server. + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncGetWithTimeout() throws Exception { + final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomLoggingFilter.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomLoggingFilter.java new file mode 100644 index 0000000..44dbc2a --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomLoggingFilter.java
@@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; + +import static org.junit.Assert.assertEquals; + +/** + * Custom logging filter. + * + * @author Santiago Pericas-Geertsen (santiago.pericasgeertsen at oracle.com) + */ +public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter, + ClientRequestFilter, ClientResponseFilter { + + private static final Logger LOGGER = Logger.getLogger(CustomLoggingFilter.class.getName()); + + static int preFilterCalled = 0; + static int postFilterCalled = 0; + + @Override + public void filter(ClientRequestContext context) throws IOException { + LOGGER.info("CustomLoggingFilter.preFilter called"); + assertEquals(context.getConfiguration().getProperty("foo"), "bar"); + preFilterCalled++; + } + + @Override + public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException { + LOGGER.info("CustomLoggingFilter.postFilter called"); + assertEquals(context.getConfiguration().getProperty("foo"), "bar"); + postFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context) throws IOException { + LOGGER.info("CustomLoggingFilter.preFilter called"); + assertEquals(context.getProperty("foo"), "bar"); + preFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException { + LOGGER.info("CustomLoggingFilter.postFilter called"); + assertEquals(context.getProperty("foo"), "bar"); + postFilterCalled++; + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HelloWorldTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HelloWorldTest.java new file mode 100644 index 0000000..d860530 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HelloWorldTest.java
@@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class HelloWorldTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName()); + private static final String ROOT_PATH = "helloworld"; + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HelloWorldResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.ASYNC_THREADPOOL_SIZE, 20); + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testConnection() { + Response response = target().path(ROOT_PATH).request("text/plain").get(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testClientStringResponse() { + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + } + + @Test + public void testAsyncClientRequests() throws InterruptedException { + final int REQUESTS = 20; + final CountDownLatch latch = new CountDownLatch(REQUESTS); + final long tic = System.currentTimeMillis(); + for (int i = 0; i < REQUESTS; i++) { + final int id = i; + target().path(ROOT_PATH).request().async().get(new InvocationCallback<Response>() { + @Override + public void completed(Response response) { + try { + final String result = response.readEntity(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, result); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(Throwable error) { + error.printStackTrace(); + latch.countDown(); + } + }); + } + assertTrue(latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS)); + final long toc = System.currentTimeMillis(); + Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic)); + } + + @Test + public void testHead() { + Response response = target().path(ROOT_PATH).request().head(); + assertEquals(200, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + } + + @Test + public void testFooBarOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals("foo/bar", response.getMediaType().toString()); + assertEquals(0, response.getLength()); + } + + @Test + public void testTextPlainOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + final String responseBody = response.readEntity(String.class); + _checkAllowContent(responseBody); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testMissingResourceNotFound() { + Response response; + + response = target().path(ROOT_PATH + "arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + + response = target().path(ROOT_PATH).path("arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + } + + @Test + public void testLoggingFilterClientClass() { + Client client = client(); + client.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + + @Test + public void testLoggingFilterClientInstance() { + Client client = client(); + client.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + + @Test + public void testLoggingFilterTargetClass() { + WebTarget target = target().path(ROOT_PATH); + target.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetInstance() { + WebTarget target = target().path(ROOT_PATH); + target.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testConfigurationUpdate() { + Client client1 = client(); + client1.register(CustomLoggingFilter.class).property("foo", "bar"); + + Client client = ClientBuilder.newClient(client1.getConfiguration()); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java new file mode 100644 index 0000000..10cfa14 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
@@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests the headers. + * + * @author Stepan Kopriva + */ +public class HttpHeadersTest extends JerseyTest { + @Path("/test") + public static class HttpMethodResource { + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + return "POST"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(HttpHeadersTest.HttpMethodResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testPost() { + Response response = target("test").request().header("X-CLIENT", "client").post(null); + + assertEquals(200, response.getStatus()); + assertTrue(response.hasEntity()); + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HugeEntityTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HugeEntityTest.java new file mode 100644 index 0000000..dbc5fac --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HugeEntityTest.java
@@ -0,0 +1,188 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.concurrent.Future; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test to make sure huge entity gets chunk-encoded. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class HugeEntityTest extends JerseyTest { + + private static final int BUFFER_LENGTH = 1024 * 1024; // 1M + private static final long HUGE_DATA_LENGTH = 20L * 1024L * 1024L * 1024L; // 20G seems sufficient + + /** + * JERSEY-2337 reproducer. The resource is used to check the right amount of data + * is being received from the client and also gives us ability to check we receive + * correct data. + */ + @Path("/") + public static class ConsumerResource { + + /** + * Return back the count of bytes received. + * This way, we should be able to consume a huge amount of data. + */ + @POST + @Path("size") + public String post(InputStream in) throws IOException { + + long totalBytesRead = 0L; + + byte[] buffer = new byte[BUFFER_LENGTH]; + int read; + do { + read = in.read(buffer); + if (read > 0) { + totalBytesRead += read; + } + } while (read != -1); + + return String.valueOf(totalBytesRead); + } + + @POST + @Path("echo") + public String echo(String s) { + return s; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(ConsumerResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(TestEntityWriter.class); + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); + config.connectorProvider(new NettyConnectorProvider()); + } + + public static class TestEntity { + + final long size; + + public TestEntity(long size) { + this.size = size; + } + } + + /** + * Utility writer that generates that many zero bytes as given by the input entity size field. + */ + public static class TestEntityWriter implements MessageBodyWriter<TestEntity> { + + @Override + public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == TestEntity.class; + } + + @Override + public long getSize(TestEntity t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; // no matter what we return here, the output will get chunk-encoded + } + + @Override + public void writeTo(TestEntity t, + Class<?> type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap<String, Object> httpHeaders, + OutputStream entityStream) throws IOException, WebApplicationException { + + final byte[] buffer = new byte[BUFFER_LENGTH]; + final long bufferCount = t.size / BUFFER_LENGTH; + final int remainder = (int) (t.size % BUFFER_LENGTH); + + for (long b = 0; b < bufferCount; b++) { + entityStream.write(buffer); + } + + if (remainder > 0) { + entityStream.write(buffer, 0, remainder); + } + } + } + + /** + * JERSEY-2337 reproducer. We are going to send huge amount of data over the wire. + * Should not the data have been chunk-encoded, we would easily run out of memory. + * + * @throws Exception in case of a test error. + */ + @Test + public void testPost() throws Exception { + Response response = target("/size").request() + .post(Entity.entity(new TestEntity(HUGE_DATA_LENGTH), + MediaType.APPLICATION_OCTET_STREAM_TYPE)); + String content = response.readEntity(String.class); + assertThat(Long.parseLong(content), equalTo(HUGE_DATA_LENGTH)); + + // just to check the right data have been transfered. + response = target("/echo").request().post(Entity.text("Hey Sync!")); + assertThat(response.readEntity(String.class), equalTo("Hey Sync!")); + } + + /** + * JERSEY-2337 reproducer. We are going to send huge amount of data over the wire. This time in an async fashion. + * Should not the data have been chunk-encoded, we would easily run out of memory. + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncPost() throws Exception { + Future<Response> response = target("/size").request().async() + .post(Entity.entity(new TestEntity(HUGE_DATA_LENGTH), + MediaType.APPLICATION_OCTET_STREAM_TYPE)); + final String content = response.get().readEntity(String.class); + assertThat(Long.parseLong(content), equalTo(HUGE_DATA_LENGTH)); + + // just to check the right data have been transfered. + response = target("/echo").request().async().post(Entity.text("Hey Async!")); + assertThat(response.get().readEntity(String.class), equalTo("Hey Async!")); + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/MethodTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/MethodTest.java new file mode 100644 index 0000000..c37cf35 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/MethodTest.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Tests the Http methods. + * + * @author Stepan Kopriva + */ +public class MethodTest extends JerseyTest { + + private static final String PATH = "test"; + + @Path("/test") + public static class HttpMethodResource { + + @GET + public String get() { + return "GET"; + } + + @POST + public String post(String entity) { + return entity; + } + + @PUT + public String put(String entity) { + return entity; + } + + @DELETE + public String delete() { + return "DELETE"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(HttpMethodResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testGet() { + Response response = target(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testPost() { + Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN)); + assertEquals("POST", response.readEntity(String.class)); + } + + @Test + public void testPut() { + Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)); + assertEquals("PUT", response.readEntity(String.class)); + } + + @Test + public void testDelete() { + Response response = target(PATH).request().delete(); + assertEquals("DELETE", response.readEntity(String.class)); + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/NoEntityTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/NoEntityTest.java new file mode 100644 index 0000000..2570b02 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/NoEntityTest.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class NoEntityTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @GET + public Response get() { + return Response.status(Response.Status.CONFLICT).build(); + } + + @POST + public void post(String entity) { + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testGet() { + WebTarget r = target("test"); + + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testGetWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testPost() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + } + } + + @Test + public void testPostWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + cr.close(); + } + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ParallelTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ParallelTest.java new file mode 100644 index 0000000..e457b31 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ParallelTest.java
@@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Assert; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests the parallel execution of multiple requests. + * + * @author Stepan Kopriva + */ +public class ParallelTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(ParallelTest.class.getName()); + + private static final int PARALLEL_CLIENTS = 10; + private static final String PATH = "test"; + private static final AtomicInteger receivedCounter = new AtomicInteger(0); + private static final AtomicInteger resourceCounter = new AtomicInteger(0); + private static final CyclicBarrier startBarrier = new CyclicBarrier(PARALLEL_CLIENTS + 1); + private static final CountDownLatch doneLatch = new CountDownLatch(PARALLEL_CLIENTS); + + @Path(PATH) + public static class MyResource { + + @GET + public String get() { + sleep(); + resourceCounter.addAndGet(1); + return "GET"; + } + + private void sleep() { + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + Logger.getLogger(ParallelTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + @Override + protected Application configure() { + return new ResourceConfig(ParallelTest.MyResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testParallel() throws BrokenBarrierException, InterruptedException, TimeoutException { + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(PARALLEL_CLIENTS); + + try { + final WebTarget target = target(); + for (int i = 1; i <= PARALLEL_CLIENTS; i++) { + final int id = i; + executor.submit(new Runnable() { + @Override + public void run() { + try { + startBarrier.await(); + Response response; + response = target.path(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + receivedCounter.incrementAndGet(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + LOGGER.log(Level.WARNING, "Client thread " + id + " interrupted.", ex); + } catch (BrokenBarrierException ex) { + LOGGER.log(Level.INFO, "Client thread " + id + " failed on broken barrier.", ex); + } catch (Throwable t) { + t.printStackTrace(); + LOGGER.log(Level.WARNING, "Client thread " + id + " failed on unexpected exception.", t); + } finally { + doneLatch.countDown(); + } + } + }); + } + + startBarrier.await(1, TimeUnit.SECONDS); + + assertTrue("Waiting for clients to finish has timed out.", doneLatch.await(5 * getAsyncTimeoutMultiplier(), + TimeUnit.SECONDS)); + + assertEquals("Resource counter", PARALLEL_CLIENTS, resourceCounter.get()); + + assertEquals("Received counter", PARALLEL_CLIENTS, receivedCounter.get()); + } finally { + executor.shutdownNow(); + Assert.assertTrue("Executor termination", executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TimeoutTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TimeoutTest.java new file mode 100644 index 0000000..d035d83 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TimeoutTest.java
@@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Martin Matula + */ +public class TimeoutTest extends JerseyTest { + @Path("/test") + public static class TimeoutResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("timeout") + public String getTimeout() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "GET"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(TimeoutResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.READ_TIMEOUT, 1000); + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testFast() { + Response r = target("test").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testSlow() { + try { + target("test/timeout").request().get(); + fail("Timeout expected."); + } catch (ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(TimeoutException.class)); + } + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TraceSupportTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TraceSupportTest.java new file mode 100644 index 0000000..8d8bf33 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/TraceSupportTest.java
@@ -0,0 +1,234 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This very basic resource showcases support of a HTTP TRACE method, + * not directly supported by JAX-RS API. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class TraceSupportTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName()); + + /** + * Programmatic tracing root resource path. + */ + public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic"; + + /** + * Annotated class-based tracing root resource path. + */ + public static final String ROOT_PATH_ANNOTATED = "tracing/annotated"; + + @HttpMethod(TRACE.NAME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TRACE { + public static final String NAME = "TRACE"; + } + + @Path(ROOT_PATH_ANNOTATED) + public static class TracingResource { + + @TRACE + @Produces("text/plain") + public String trace(Request request) { + return stringify((ContainerRequest) request); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TracingResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC); + resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector<ContainerRequestContext, Response>() { + + @Override + public Response apply(ContainerRequestContext request) { + if (request == null) { + return Response.noContent().build(); + } else { + return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build(); + } + } + }); + + return config.registerResources(resourceBuilder.build()); + + } + + private String[] expectedFragmentsProgrammatic = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic" + }; + private String[] expectedFragmentsAnnotated = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/annotated" + }; + + private WebTarget prepareTarget(String path) { + final WebTarget target = target(); + target.register(LoggingFeature.class); + return target.path(path); + } + + @Test + public void testProgrammaticApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsProgrammatic) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testAnnotatedApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsAnnotated) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testTraceWithEntity() throws Exception { + _testTraceWithEntity(false, false); + } + + @Test + public void testAsyncTraceWithEntity() throws Exception { + _testTraceWithEntity(true, false); + } + + @Test + public void testTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(false, true); + } + + @Test + public void testAsyncTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(true, true); + } + + private void _testTraceWithEntity(final boolean isAsync, final boolean useGrizzlyConnector) throws Exception { + try { + WebTarget target = useGrizzlyConnector ? createGrizzlyClient().target(target().getUri()) : target(); + target = target.path(ROOT_PATH_ANNOTATED); + + final Entity<String> entity = Entity.entity("trace", MediaType.WILDCARD_TYPE); + + Response response; + if (!isAsync) { + response = target.request().method(TRACE.NAME, entity); + } else { + response = target.request().async().method(TRACE.NAME, entity).get(); + } + + fail("A TRACE request MUST NOT include an entity. (response=" + response + ")"); + } catch (Exception e) { + // OK + } + } + + private Client createGrizzlyClient() { + return ClientBuilder.newClient(new ClientConfig().connectorProvider(new NettyConnectorProvider())); + } + + + public static String stringify(ContainerRequest request) { + StringBuilder buffer = new StringBuilder(); + + printRequestLine(buffer, request); + printPrefixedHeaders(buffer, request.getHeaders()); + + if (request.hasEntity()) { + buffer.append(request.readEntity(String.class)).append("\n"); + } + + return buffer.toString(); + } + + private static void printRequestLine(StringBuilder buffer, ContainerRequest request) { + buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n"); + } + + private static void printPrefixedHeaders(StringBuilder buffer, Map<String, List<String>> headers) { + for (Map.Entry<String, List<String>> e : headers.entrySet()) { + List<String> val = e.getValue(); + String header = e.getKey(); + + if (val.size() == 1) { + buffer.append(header).append(": ").append(val.get(0)).append("\n"); + } else { + StringBuilder sb = new StringBuilder(); + boolean add = false; + for (String s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + buffer.append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } +}
diff --git a/connectors/pom.xml b/connectors/pom.xml new file mode 100644 index 0000000..f48fcb7 --- /dev/null +++ b/connectors/pom.xml
@@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>project</artifactId> + <packaging>pom</packaging> + <name>jersey-connectors</name> + + <description>Jersey client connection providers umbrella project module</description> + + <modules> + <module>apache-connector</module> + <module>grizzly-connector</module> + <module>jdk-connector</module> + <module>jetty-connector</module> + <module>netty-connector</module> + </modules> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project>
diff --git a/containers/glassfish/jersey-gf-ejb/pom.xml b/containers/glassfish/jersey-gf-ejb/pom.xml new file mode 100644 index 0000000..4038ce7 --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/pom.xml
@@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers.glassfish</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-gf-ejb</artifactId> + <packaging>jar</packaging> + <name>jersey-gf-ejb</name> + + <description>Jersey EJB for GlassFish integration</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.ext.cdi</groupId> + <artifactId>jersey-cdi1x</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.ejb</groupId> + <artifactId>javax.ejb-api</artifactId> + <version>${ejb.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.interceptor</groupId> + <artifactId>javax.interceptor-api</artifactId> + <version>${javax.interceptor.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.main.ejb</groupId> + <artifactId>ejb-container</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.main.common</groupId> + <artifactId>container-common</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <!-- + Excluding only *.tests.* packages for this module. At least one class has to exist in non-excluded + packages to generate proper -javadoc.jar file. See https://jira.codehaus.org/browse/MJAVADOC-329 + --> + <excludePackageNames>*.tests.*</excludePackageNames> + </configuration> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- Explicitly set versions for packages from GlassFish to allow future uptake of GlassFish 5.x--> + <Import-Package> + com.sun.*;version="[4.0,6)", + org.glassfish.ejb.*;version="[4.0,6)", + org.glassfish.internal.*;version="[4.0,6)", + * + </Import-Package> + <_versionpolicy>[$(version;==;$(@)),$(version;+;$(@)))</_versionpolicy> + <_nodefaultversion>false</_nodefaultversion> + </instructions> + </configuration> + <executions> + <execution> + <id>osgi-bundle</id> + <phase>package</phase> + <goals> + <goal>bundle</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project>
diff --git a/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbComponentInterceptor.java b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbComponentInterceptor.java new file mode 100644 index 0000000..26e6e8b --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbComponentInterceptor.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gf.ejb.internal; + +import javax.annotation.PostConstruct; +import javax.interceptor.InvocationContext; + +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.internal.inject.InjectionManager; + +/** + * EJB interceptor to inject Jersey specific stuff into EJB beans. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public final class EjbComponentInterceptor { + + private final InjectionManager injectionManager; + + /** + * Create new EJB component injection manager. + * + * @param injectionManager injection manager. + */ + public EjbComponentInterceptor(final InjectionManager injectionManager) { + this.injectionManager = injectionManager; + } + + @PostConstruct + private void inject(final InvocationContext context) throws Exception { + + final Object beanInstance = context.getTarget(); + injectionManager.inject(beanInstance, CdiComponentProvider.CDI_CLASS_ANALYZER); + + // Invoke next interceptor in chain + context.proceed(); + } +}
diff --git a/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbComponentProvider.java b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbComponentProvider.java new file mode 100644 index 0000000..8a652e3 --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbComponentProvider.java
@@ -0,0 +1,415 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gf.ejb.internal; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ext.ExceptionMapper; + +import javax.annotation.Priority; +import javax.ejb.Local; +import javax.ejb.Remote; +import javax.inject.Singleton; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.Binding; +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InstanceBinding; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.model.Invocable; +import org.glassfish.jersey.server.spi.ComponentProvider; +import org.glassfish.jersey.server.spi.internal.ResourceMethodInvocationHandlerProvider; + +import org.glassfish.ejb.deployment.descriptor.EjbBundleDescriptorImpl; +import org.glassfish.ejb.deployment.descriptor.EjbDescriptor; +import org.glassfish.internal.data.ApplicationInfo; +import org.glassfish.internal.data.ApplicationRegistry; +import org.glassfish.internal.data.ModuleInfo; + +import com.sun.ejb.containers.BaseContainer; +import com.sun.ejb.containers.EjbContainerUtil; +import com.sun.ejb.containers.EjbContainerUtilImpl; +import com.sun.enterprise.config.serverbeans.Application; +import com.sun.enterprise.config.serverbeans.Applications; + +/** + * EJB component provider. + * + * @author Paul Sandoz + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +@Priority(300) +@SuppressWarnings("UnusedDeclaration") +public final class EjbComponentProvider implements ComponentProvider, ResourceMethodInvocationHandlerProvider { + + private static final Logger LOGGER = Logger.getLogger(EjbComponentProvider.class.getName()); + + private InitialContext initialContext; + private final List<String> libNames = new CopyOnWriteArrayList<>(); + + private boolean ejbInterceptorRegistered = false; + + /** + * HK2 factory to provide EJB components obtained via JNDI lookup. + */ + private static class EjbFactory<T> implements Supplier<T> { + + final InitialContext ctx; + final Class<T> clazz; + final EjbComponentProvider ejbProvider; + + @SuppressWarnings("unchecked") + @Override + public T get() { + try { + return (T) lookup(ctx, clazz, clazz.getSimpleName(), ejbProvider); + } catch (NamingException ex) { + Logger.getLogger(ApplicationHandler.class.getName()).log(Level.SEVERE, null, ex); + return null; + } + } + + public EjbFactory(Class<T> rawType, InitialContext ctx, EjbComponentProvider ejbProvider) { + this.clazz = rawType; + this.ctx = ctx; + this.ejbProvider = ejbProvider; + } + } + + /** + * Annotations to determine EJB components. + */ + private static final Set<String> EjbComponentAnnotations = Collections.unmodifiableSet(new HashSet<String>() {{ + add("javax.ejb.Stateful"); + add("javax.ejb.Stateless"); + add("javax.ejb.Singleton"); + }}); + + private InjectionManager injectionManager = null; + + // ComponentProvider + @Override + public void initialize(final InjectionManager injectionManager) { + this.injectionManager = injectionManager; + + InstanceBinding<EjbComponentProvider> descriptor = Bindings.service(EjbComponentProvider.this) + .to(ResourceMethodInvocationHandlerProvider.class); + this.injectionManager.register(descriptor); + } + + private ApplicationInfo getApplicationInfo(EjbContainerUtil ejbUtil) throws NamingException { + ApplicationRegistry appRegistry = ejbUtil.getServices().getService(ApplicationRegistry.class); + Applications applications = ejbUtil.getServices().getService(Applications.class); + String appNamePrefix = (String) initialContext.lookup("java:app/AppName"); + Set<String> appNames = appRegistry.getAllApplicationNames(); + Set<String> disabledApps = new TreeSet<>(); + for (String appName : appNames) { + if (appName.startsWith(appNamePrefix)) { + Application appDesc = applications.getApplication(appName); + if (appDesc != null && !ejbUtil.getDeployment().isAppEnabled(appDesc)) { + // skip disabled version of the app + disabledApps.add(appName); + } else { + return ejbUtil.getDeployment().get(appName); + } + } + } + + // grab the latest one, there is no way to make + // sure which one the user is actually enabling, + // so use the best case, i.e. upgrade + Iterator<String> it = disabledApps.iterator(); + String lastDisabledApp = null; + while (it.hasNext()) { + lastDisabledApp = it.next(); + } + if (lastDisabledApp != null) { + return ejbUtil.getDeployment().get(lastDisabledApp); + } + + throw new NamingException("Application Information Not Found"); + } + + private void registerEjbInterceptor() { + try { + final Object interceptor = new EjbComponentInterceptor(injectionManager); + initialContext = getInitialContext(); + final EjbContainerUtil ejbUtil = EjbContainerUtilImpl.getInstance(); + final ApplicationInfo appInfo = getApplicationInfo(ejbUtil); + final List<String> tempLibNames = new LinkedList<>(); + for (ModuleInfo moduleInfo : appInfo.getModuleInfos()) { + final String jarName = moduleInfo.getName(); + if (jarName.endsWith(".jar") || jarName.endsWith(".war")) { + final String moduleName = jarName.substring(0, jarName.length() - 4); + tempLibNames.add(moduleName); + final Object bundleDescriptor = moduleInfo.getMetaData(EjbBundleDescriptorImpl.class.getName()); + if (bundleDescriptor instanceof EjbBundleDescriptorImpl) { + final Collection<EjbDescriptor> ejbs = ((EjbBundleDescriptorImpl) bundleDescriptor).getEjbs(); + + for (final EjbDescriptor ejb : ejbs) { + final BaseContainer ejbContainer = EjbContainerUtilImpl.getInstance().getContainer(ejb.getUniqueId()); + try { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public Object run() throws Exception { + final Method registerInterceptorMethod = + BaseContainer.class + .getDeclaredMethod("registerSystemInterceptor", java.lang.Object.class); + registerInterceptorMethod.setAccessible(true); + + registerInterceptorMethod.invoke(ejbContainer, interceptor); + return null; + } + }); + } catch (PrivilegedActionException pae) { + final Throwable cause = pae.getCause(); + LOGGER.log(Level.WARNING, + LocalizationMessages.EJB_INTERCEPTOR_BINDING_WARNING(ejb.getEjbClassName()), cause); + } + } + } + } + } + libNames.addAll(tempLibNames); + final Object interceptorBinder = initialContext.lookup("java:org.glassfish.ejb.container.interceptor_binding_spi"); + // Some implementations of InitialContext return null instead of + // throwing NamingException if there is no Object associated with + // the name + if (interceptorBinder == null) { + throw new IllegalStateException(LocalizationMessages.EJB_INTERCEPTOR_BIND_API_NOT_AVAILABLE()); + } + + try { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public Object run() throws Exception { + Method interceptorBinderMethod = interceptorBinder.getClass() + .getMethod("registerInterceptor", java.lang.Object.class); + + interceptorBinderMethod.invoke(interceptorBinder, interceptor); + EjbComponentProvider.this.ejbInterceptorRegistered = true; + LOGGER.log(Level.CONFIG, LocalizationMessages.EJB_INTERCEPTOR_BOUND()); + return null; + } + }); + } catch (PrivilegedActionException pae) { + throw new IllegalStateException(LocalizationMessages.EJB_INTERCEPTOR_CONFIG_ERROR(), pae.getCause()); + } + + } catch (NamingException ex) { + throw new IllegalStateException(LocalizationMessages.EJB_INTERCEPTOR_BIND_API_NOT_AVAILABLE(), ex); + } catch (LinkageError ex) { + throw new IllegalStateException(LocalizationMessages.EJB_INTERCEPTOR_CONFIG_LINKAGE_ERROR(), ex); + } + } + + // ComponentProvider + @SuppressWarnings("unchecked") + @Override + public boolean bind(Class<?> component, Set<Class<?>> providerContracts) { + + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(LocalizationMessages.EJB_CLASS_BEING_CHECKED(component)); + } + + if (injectionManager == null) { + throw new IllegalStateException(LocalizationMessages.EJB_COMPONENT_PROVIDER_NOT_INITIALIZED_PROPERLY()); + } + + if (!isEjbComponent(component)) { + return false; + } + + if (!ejbInterceptorRegistered) { + registerEjbInterceptor(); + } + + Binding binding = Bindings.supplier(new EjbFactory(component, initialContext, EjbComponentProvider.this)) + .to(component) + .to(providerContracts); + injectionManager.register(binding); + + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.config(LocalizationMessages.EJB_CLASS_BOUND_WITH_CDI(component)); + } + + return true; + } + + @Override + public void done() { + registerEjbExceptionMapper(); + } + + private void registerEjbExceptionMapper() { + injectionManager.register(new AbstractBinder() { + @Override + protected void configure() { + bind(EjbExceptionMapper.class).to(ExceptionMapper.class).in(Singleton.class); + } + }); + } + + private boolean isEjbComponent(Class<?> component) { + for (Annotation a : component.getAnnotations()) { + if (EjbComponentAnnotations.contains(a.annotationType().getName())) { + return true; + } + } + return false; + } + + @Override + public InvocationHandler create(Invocable method) { + + final Class<?> resourceClass = method.getHandler().getHandlerClass(); + + if (resourceClass == null || !isEjbComponent(resourceClass)) { + return null; + } + + final Method handlingMethod = method.getDefinitionMethod(); + + for (Class iFace : remoteAndLocalIfaces(resourceClass)) { + try { + final Method iFaceMethod = iFace.getDeclaredMethod(handlingMethod.getName(), handlingMethod.getParameterTypes()); + if (iFaceMethod != null) { + return new InvocationHandler() { + @Override + public Object invoke(Object target, Method ignored, Object[] args) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return iFaceMethod.invoke(target, args); + } + }; + } + } catch (NoSuchMethodException | SecurityException ex) { + logLookupException(handlingMethod, resourceClass, iFace, ex); + } + } + return null; + } + + private void logLookupException(final Method method, final Class<?> component, Class<?> iFace, Exception ex) { + LOGGER.log( + Level.WARNING, + LocalizationMessages.EJB_INTERFACE_HANDLING_METHOD_LOOKUP_EXCEPTION(method, component, iFace), ex); + } + + private List<Class> remoteAndLocalIfaces(final Class<?> resourceClass) { + final List<Class> allLocalOrRemoteIfaces = new LinkedList<>(); + if (resourceClass.isAnnotationPresent(Remote.class)) { + allLocalOrRemoteIfaces.addAll(Arrays.asList(resourceClass.getAnnotation(Remote.class).value())); + } + if (resourceClass.isAnnotationPresent(Local.class)) { + allLocalOrRemoteIfaces.addAll(Arrays.asList(resourceClass.getAnnotation(Local.class).value())); + } + for (Class<?> i : resourceClass.getInterfaces()) { + if (i.isAnnotationPresent(Remote.class) || i.isAnnotationPresent(Local.class)) { + allLocalOrRemoteIfaces.add(i); + } + } + return allLocalOrRemoteIfaces; + } + + private static InitialContext getInitialContext() { + try { + // Deployment on Google App Engine will + // result in a LinkageError + return new InitialContext(); + } catch (Exception ex) { + throw new IllegalStateException(LocalizationMessages.INITIAL_CONTEXT_NOT_AVAILABLE(), ex); + } + } + + private static Object lookup(InitialContext ic, Class<?> c, String name, EjbComponentProvider provider) + throws NamingException { + try { + return lookupSimpleForm(ic, name, provider); + } catch (NamingException ex) { + LOGGER.log(Level.WARNING, LocalizationMessages.EJB_CLASS_SIMPLE_LOOKUP_FAILED(c.getName()), ex); + + return lookupFullyQualifiedForm(ic, c, name, provider); + } + } + + private static Object lookupSimpleForm(InitialContext ic, String name, EjbComponentProvider provider) throws NamingException { + if (provider.libNames.isEmpty()) { + String jndiName = "java:module/" + name; + return ic.lookup(jndiName); + } else { + NamingException ne = null; + for (String moduleName : provider.libNames) { + String jndiName = "java:app/" + moduleName + "/" + name; + Object result; + try { + result = ic.lookup(jndiName); + if (result != null) { + return result; + } + } catch (NamingException e) { + ne = e; + } + } + throw (ne != null) ? ne : new NamingException(); + } + } + + private static Object lookupFullyQualifiedForm(InitialContext ic, Class<?> c, String name, EjbComponentProvider provider) + throws NamingException { + if (provider.libNames.isEmpty()) { + String jndiName = "java:module/" + name + "!" + c.getName(); + return ic.lookup(jndiName); + } else { + NamingException ne = null; + for (String moduleName : provider.libNames) { + String jndiName = "java:app/" + moduleName + "/" + name + "!" + c.getName(); + Object result; + try { + result = ic.lookup(jndiName); + if (result != null) { + return result; + } + } catch (NamingException e) { + ne = e; + } + } + throw (ne != null) ? ne : new NamingException(); + } + } +}
diff --git a/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbExceptionMapper.java b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbExceptionMapper.java new file mode 100644 index 0000000..1121c41 --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/EjbExceptionMapper.java
@@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.gf.ejb.internal; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import javax.ejb.EJBException; +import javax.inject.Inject; +import javax.inject.Provider; + +import org.glassfish.jersey.spi.ExceptionMappers; +import org.glassfish.jersey.spi.ExtendedExceptionMapper; + +/** + * Helper class to handle exceptions wrapped by the EJB container with EJBException. + * If this mapper was not registered, no {@link WebApplicationException} + * would end up mapped to the corresponding response. + * + * @author Paul Sandoz + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class EjbExceptionMapper implements ExtendedExceptionMapper<EJBException> { + + private final Provider<ExceptionMappers> mappers; + + /** + * Create new EJB exception mapper. + * + * @param mappers utility to find mapper delegate. + */ + @Inject + public EjbExceptionMapper(Provider<ExceptionMappers> mappers) { + this.mappers = mappers; + } + + @Override + public Response toResponse(EJBException exception) { + return causeToResponse(exception); + } + + @Override + public boolean isMappable(EJBException exception) { + try { + return (causeToResponse(exception) != null); + } catch (Throwable ignored) { + return false; + } + } + + private Response causeToResponse(EJBException exception) { + + final Exception cause = exception.getCausedByException(); + + if (cause != null) { + + final ExceptionMapper mapper = mappers.get().findMapping(cause); + if (mapper != null && mapper != this) { + + return mapper.toResponse(cause); + + } else if (cause instanceof WebApplicationException) { + + return ((WebApplicationException) cause).getResponse(); + } + } + return null; + } +}
diff --git a/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/package-info.java b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/package-info.java new file mode 100644 index 0000000..700cfe7 --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/src/main/java/org/glassfish/jersey/gf/ejb/internal/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey internal package supporting Jersey EJB injections in Glassfish 4 environment. + */ +package org.glassfish.jersey.gf.ejb.internal;
diff --git a/containers/glassfish/jersey-gf-ejb/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ComponentProvider b/containers/glassfish/jersey-gf-ejb/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ComponentProvider new file mode 100644 index 0000000..2db13b7 --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ComponentProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.gf.ejb.internal.EjbComponentProvider
diff --git a/containers/glassfish/jersey-gf-ejb/src/main/resources/org/glassfish/jersey/gf/ejb/internal/localization.properties b/containers/glassfish/jersey-gf-ejb/src/main/resources/org/glassfish/jersey/gf/ejb/internal/localization.properties new file mode 100644 index 0000000..95bbf1c --- /dev/null +++ b/containers/glassfish/jersey-gf-ejb/src/main/resources/org/glassfish/jersey/gf/ejb/internal/localization.properties
@@ -0,0 +1,29 @@ +# +# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +initial.context.not.available=InitialContext not found. JAX-RS EJB support is not available. +ejb.interceptor.bind.api.not.available=The EJB interceptor binding API is not available. JAX-RS EJB integration can not be supported. +ejb.interceptor.bind.api.non.conformant=The EJB interceptor binding API does not conform to what is expected. JAX-RS EJB integration can not be supported. +ejb.interceptor.binding.warning=Could not bind EJB intercetor for class, {0}. +ejb.interceptor.bound=The Jersey EJB interceptor is bound. JAX-RS EJB integration support is enabled. +ejb.interceptor.config.error=Error when configuring to use the EJB interceptor binding API. JAX-RS EJB integration can not be supported. +ejb.interceptor.config.security.error=Security issue when configuring to use the EJB interceptor binding API. JAX-RS EJB support is not available. +ejb.interceptor.config.linkage.error=Linkage error when configuring to use the EJB interceptor binding API. JAX-RS EJB integration can not be supported. +ejb.component.provider.not.initialized.properly=EJB component provider has not been initialized properly. +ejb.class.simple.lookup.failed=An instance of EJB class, {0}, could not be looked up using simple form name. Attempting to look up using the fully-qualified form name. +ejb.interface.handling.method.lookup.exception=Exception thrown when trying to lookup actual handling method, {0}, for EJB type, {1}, using interface {2}. +ejb.class.being.checked=Class, {0}, is being checked with Jersey EJB component provider. +ejb.class.bound.with.cdi=Class, {0}, has been bound by Jersey EJB component provider.
diff --git a/containers/glassfish/pom.xml b/containers/glassfish/pom.xml new file mode 100644 index 0000000..c10d92e --- /dev/null +++ b/containers/glassfish/pom.xml
@@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.containers.glassfish</groupId> + <artifactId>project</artifactId> + <packaging>pom</packaging> + <name>jersey-glassfish-support</name> + + <description>Jersey GlassFish container providers umbrella project module</description> + + <modules> + <module>jersey-gf-ejb</module> + </modules> + +</project>
diff --git a/containers/grizzly2-http/pom.xml b/containers/grizzly2-http/pom.xml new file mode 100644 index 0000000..49e75e5 --- /dev/null +++ b/containers/grizzly2-http/pom.xml
@@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-grizzly2-http</artifactId> + <packaging>jar</packaging> + <name>jersey-container-grizzly2-http</name> + + <description>Grizzly 2 Http Container.</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.grizzly</groupId> + <artifactId>grizzly-http-server</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + </plugins> + + <resources> + <resource> + <directory>${basedir}/src/main/java</directory> + <includes> + <include>META-INF/**/*</include> + </includes> + </resource> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> + +</project>
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java new file mode 100644 index 0000000..8c9815d --- /dev/null +++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java
@@ -0,0 +1,485 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly2.httpserver; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.SecurityContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.glassfish.jersey.grizzly2.httpserver.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.ExtendedLogger; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.internal.ContainerUtils; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import org.glassfish.grizzly.CompletionHandler; +import org.glassfish.grizzly.http.server.HttpHandler; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +/** + * Jersey {@code Container} implementation based on Grizzly {@link org.glassfish.grizzly.http.server.HttpHandler}. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Libor Kramolis (libor.kramolis at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class GrizzlyHttpContainer extends HttpHandler implements Container { + + private static final ExtendedLogger logger = + new ExtendedLogger(Logger.getLogger(GrizzlyHttpContainer.class.getName()), Level.FINEST); + + private final Type RequestTYPE = (new GenericType<Ref<Request>>() { }).getType(); + private final Type ResponseTYPE = (new GenericType<Ref<Response>>() { }).getType(); + + /** + * Cached value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR}. + * If {@code true} method {@link org.glassfish.grizzly.http.server.Response#setStatus} is used over + * {@link org.glassfish.grizzly.http.server.Response#sendError}. + */ + private boolean configSetStatusOverSendError; + + /** + * Cached value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#REDUCE_CONTEXT_PATH_SLASHES_ENABLED}. + * If {@code true} method {@link org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer#getRequestUri(Request)} + * will reduce the of leading context-path slashes to only one. + */ + private boolean configReduceContextPathSlashesEnabled; + + /** + * Referencing factory for Grizzly request. + */ + private static class GrizzlyRequestReferencingFactory extends ReferencingFactory<Request> { + + @Inject + public GrizzlyRequestReferencingFactory(final Provider<Ref<Request>> referenceFactory) { + super(referenceFactory); + } + } + + /** + * Referencing factory for Grizzly response. + */ + private static class GrizzlyResponseReferencingFactory extends ReferencingFactory<Response> { + + @Inject + public GrizzlyResponseReferencingFactory(final Provider<Ref<Response>> referenceFactory) { + super(referenceFactory); + } + } + + /** + * An internal binder to enable Grizzly HTTP container specific types injection. + * <p/> + * This binder allows to inject underlying Grizzly HTTP request and response instances. + * Note that since Grizzly {@code Request} class is not proxiable as it does not expose an empty constructor, + * the injection of Grizzly request instance into singleton JAX-RS and Jersey providers is only supported via + * {@link javax.inject.Provider injection provider}. + */ + static class GrizzlyBinder extends AbstractBinder { + + @Override + protected void configure() { + bindFactory(GrizzlyRequestReferencingFactory.class).to(Request.class) + .proxy(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.<Request>referenceFactory()).to(new GenericType<Ref<Request>>() {}) + .in(RequestScoped.class); + + bindFactory(GrizzlyResponseReferencingFactory.class).to(Response.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.<Response>referenceFactory()).to(new GenericType<Ref<Response>>() {}) + .in(RequestScoped.class); + } + } + + private static final CompletionHandler<Response> EMPTY_COMPLETION_HANDLER = new CompletionHandler<Response>() { + + @Override + public void cancelled() { + // no-op + } + + @Override + public void failed(final Throwable throwable) { + // no-op + } + + @Override + public void completed(final Response result) { + // no-op + } + + @Override + public void updated(final Response result) { + // no-op + } + }; + + private static final class ResponseWriter implements ContainerResponseWriter { + + private final String name; + private final Response grizzlyResponse; + private final boolean configSetStatusOverSendError; + + ResponseWriter(final Response response, final boolean configSetStatusOverSendError) { + this.grizzlyResponse = response; + this.configSetStatusOverSendError = configSetStatusOverSendError; + + if (logger.isDebugLoggable()) { + this.name = "ResponseWriter {" + "id=" + UUID.randomUUID().toString() + ", grizzlyResponse=" + + grizzlyResponse.hashCode() + '}'; + logger.debugLog("{0} - init", name); + } else { + this.name = "ResponseWriter"; + } + } + + @Override + public String toString() { + return name; + } + + @Override + public void commit() { + try { + if (grizzlyResponse.isSuspended()) { + grizzlyResponse.resume(); + } + } finally { + logger.debugLog("{0} - commit() called", name); + } + } + + @Override + public boolean suspend(final long timeOut, final TimeUnit timeUnit, final TimeoutHandler timeoutHandler) { + try { + grizzlyResponse.suspend(timeOut, timeUnit, EMPTY_COMPLETION_HANDLER, + new org.glassfish.grizzly.http.server.TimeoutHandler() { + + @Override + public boolean onTimeout(final Response response) { + if (timeoutHandler != null) { + timeoutHandler.onTimeout(ResponseWriter.this); + } + + // TODO should we return true in some cases instead? + // Returning false relies on the fact that the timeoutHandler will resume the response. + return false; + } + } + ); + return true; + } catch (final IllegalStateException ex) { + return false; + } finally { + logger.debugLog("{0} - suspend(...) called", name); + } + } + + @Override + public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throws IllegalStateException { + try { + grizzlyResponse.getSuspendContext().setTimeout(timeOut, timeUnit); + } finally { + logger.debugLog("{0} - setTimeout(...) called", name); + } + } + + @Override + public OutputStream writeResponseStatusAndHeaders(final long contentLength, + final ContainerResponse context) + throws ContainerException { + try { + final javax.ws.rs.core.Response.StatusType statusInfo = context.getStatusInfo(); + if (statusInfo.getReasonPhrase() == null) { + grizzlyResponse.setStatus(statusInfo.getStatusCode()); + } else { + grizzlyResponse.setStatus(statusInfo.getStatusCode(), statusInfo.getReasonPhrase()); + } + + grizzlyResponse.setContentLengthLong(contentLength); + + for (final Map.Entry<String, List<String>> e : context.getStringHeaders().entrySet()) { + for (final String value : e.getValue()) { + grizzlyResponse.addHeader(e.getKey(), value); + } + } + + return grizzlyResponse.getOutputStream(); + } finally { + logger.debugLog("{0} - writeResponseStatusAndHeaders() called", name); + } + } + + @Override + @SuppressWarnings("MagicNumber") + public void failure(final Throwable error) { + try { + if (!grizzlyResponse.isCommitted()) { + try { + if (configSetStatusOverSendError) { + grizzlyResponse.reset(); + grizzlyResponse.setStatus(500, "Request failed."); + } else { + grizzlyResponse.sendError(500, "Request failed."); + } + } catch (final IllegalStateException ex) { + // a race condition externally committing the response can still occur... + logger.log(Level.FINER, "Unable to reset failed response.", ex); + } catch (final IOException ex) { + throw new ContainerException( + LocalizationMessages.EXCEPTION_SENDING_ERROR_RESPONSE(500, "Request failed."), + ex); + } + } + } finally { + logger.debugLog("{0} - failure(...) called", name); + rethrow(error); + } + } + + @Override + public boolean enableResponseBuffering() { + return true; + } + + /** + * Rethrow the original exception as required by JAX-RS, 3.3.4 + * + * @param error throwable to be re-thrown + */ + private void rethrow(final Throwable error) { + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new ContainerException(error); + } + } + } + + private volatile ApplicationHandler appHandler; + + /** + * Create a new Grizzly HTTP container. + * + * @param application JAX-RS / Jersey application to be deployed on Grizzly HTTP container. + */ + /* package */ GrizzlyHttpContainer(final Application application) { + this.appHandler = new ApplicationHandler(application, new GrizzlyBinder()); + cacheConfigSetStatusOverSendError(); + cacheConfigEnableLeadingContextPathSlashes(); + } + + /** + * Create a new Grizzly HTTP container. + * + * @param application JAX-RS / Jersey application to be deployed on Grizzly HTTP container. + * @param parentContext DI provider specific context with application's registered bindings. + */ + /* package */ GrizzlyHttpContainer(final Application application, final Object parentContext) { + this.appHandler = new ApplicationHandler(application, new GrizzlyBinder(), parentContext); + cacheConfigSetStatusOverSendError(); + cacheConfigEnableLeadingContextPathSlashes(); + } + + @Override + public void start() { + super.start(); + appHandler.onStartup(this); + } + + @Override + public void service(final Request request, final Response response) { + final ResponseWriter responseWriter = new ResponseWriter(response, configSetStatusOverSendError); + try { + logger.debugLog("GrizzlyHttpContainer.service(...) started"); + URI baseUri = getBaseUri(request); + URI requestUri = getRequestUri(request); + final ContainerRequest requestContext = new ContainerRequest(baseUri, + requestUri, request.getMethod().getMethodString(), + getSecurityContext(request), new GrizzlyRequestPropertiesDelegate(request)); + requestContext.setEntityStream(request.getInputStream()); + for (final String headerName : request.getHeaderNames()) { + requestContext.headers(headerName, request.getHeaders(headerName)); + } + requestContext.setWriter(responseWriter); + + requestContext.setRequestScopedInitializer(injectionManager -> { + injectionManager.<Ref<Request>>getInstance(RequestTYPE).set(request); + injectionManager.<Ref<Response>>getInstance(ResponseTYPE).set(response); + }); + appHandler.handle(requestContext); + } finally { + logger.debugLog("GrizzlyHttpContainer.service(...) finished"); + } + } + + private boolean containsContextPath(Request request) { + return request.getContextPath() != null && request.getContextPath().length() > 0; + } + + @Override + public ResourceConfig getConfiguration() { + return appHandler.getConfiguration(); + } + + @Override + public void reload() { + reload(appHandler.getConfiguration()); + } + + @Override + public void reload(final ResourceConfig configuration) { + appHandler.onShutdown(this); + + appHandler = new ApplicationHandler(configuration, new GrizzlyBinder()); + appHandler.onReload(this); + appHandler.onStartup(this); + cacheConfigSetStatusOverSendError(); + cacheConfigEnableLeadingContextPathSlashes(); + } + + @Override + public ApplicationHandler getApplicationHandler() { + return appHandler; + } + + @Override + public void destroy() { + super.destroy(); + appHandler.onShutdown(this); + appHandler = null; + } + + private SecurityContext getSecurityContext(final Request request) { + return new SecurityContext() { + + @Override + public boolean isUserInRole(final String role) { + return false; + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public Principal getUserPrincipal() { + return request.getUserPrincipal(); + } + + @Override + public String getAuthenticationScheme() { + return request.getAuthType(); + } + }; + } + + private URI getBaseUri(final Request request) { + try { + return new URI(request.getScheme(), null, request.getServerName(), + request.getServerPort(), getBasePath(request), null, null); + } catch (final URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private String getBasePath(final Request request) { + final String contextPath = request.getContextPath(); + + if (contextPath == null || contextPath.isEmpty()) { + return "/"; + } else if (contextPath.charAt(contextPath.length() - 1) != '/') { + return contextPath + "/"; + } else { + return contextPath; + } + } + + private URI getRequestUri(final Request request) { + try { + final String serverAddress = getServerAddress(request); + + String uri; + if (configReduceContextPathSlashesEnabled && containsContextPath(request)) { + uri = ContainerUtils.reduceLeadingSlashes(request.getRequestURI()); + } else { + uri = request.getRequestURI(); + } + + final String queryString = request.getQueryString(); + if (queryString != null) { + uri = uri + "?" + ContainerUtils.encodeUnsafeCharacters(queryString); + } + + return new URI(serverAddress + uri); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private String getServerAddress(final Request request) throws URISyntaxException { + return new URI(request.getScheme(), null, request.getServerName(), request.getServerPort(), null, null, null).toString(); + } + + /** + * The method reads and caches value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR} for future purposes. + */ + private void cacheConfigSetStatusOverSendError() { + this.configSetStatusOverSendError = ServerProperties.getValue(getConfiguration().getProperties(), + ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, false, Boolean.class); + } + + /** + * The method reads and caches value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#REDUCE_CONTEXT_PATH_SLASHES_ENABLED} for future purposes. + */ + private void cacheConfigEnableLeadingContextPathSlashes() { + this.configReduceContextPathSlashesEnabled = ServerProperties.getValue(getConfiguration().getProperties(), + ServerProperties.REDUCE_CONTEXT_PATH_SLASHES_ENABLED, false, Boolean.class); + } +}
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainerProvider.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainerProvider.java new file mode 100644 index 0000000..4d14227 --- /dev/null +++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainerProvider.java
@@ -0,0 +1,42 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly2.httpserver; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.server.spi.ContainerProvider; + +import org.glassfish.grizzly.http.server.HttpHandler; + +/** + * Container provider for containers based on Grizzly {@link org.glassfish.grizzly.http.server.HttpHandler}. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class GrizzlyHttpContainerProvider implements ContainerProvider { + + @Override + public <T> T createContainer(Class<T> type, Application application) throws ProcessingException { + if (HttpHandler.class == type || GrizzlyHttpContainer.class == type) { + return type.cast(new GrizzlyHttpContainer(application)); + } + + return null; + } +}
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java new file mode 100644 index 0000000..fed3ec7 --- /dev/null +++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
@@ -0,0 +1,282 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly2.httpserver; + +import java.io.IOException; +import java.net.URI; + +import javax.ws.rs.ProcessingException; + +import org.glassfish.jersey.grizzly2.httpserver.internal.LocalizationMessages; +import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder; +import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; + +import org.glassfish.grizzly.http.server.HttpHandler; +import org.glassfish.grizzly.http.server.HttpHandlerRegistration; +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.grizzly.http.server.NetworkListener; +import org.glassfish.grizzly.http.server.ServerConfiguration; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; +import org.glassfish.grizzly.utils.Charsets; + +/** + * Factory for creating Grizzly Http Server. + * <p> + * Should you need to fine tune the underlying Grizzly transport layer, you can obtain direct access to the corresponding + * Grizzly structures with <tt>server.getListener("grizzly").getTransport()</tt>. To make certain options take effect, + * you need to work with an inactive HttpServer instance (that is the one that has not been started yet). + * To obtain such an instance, use one of the below factory methods with {@code start} parameter set to {@code false}. + * When the {@code start} parameter is not present, the factory method returns an already started instance. + * </p> + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + * @see HttpServer + * @see GrizzlyHttpContainer + */ +public final class GrizzlyHttpServerFactory { + + /** + * Create new {@link HttpServer} instance. + * + * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path segment will be used as + * context path, the rest will be ignored. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + */ + public static HttpServer createHttpServer(final URI uri) { + return createHttpServer(uri, (GrizzlyHttpContainer) null, false, null, true); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path segment will be used + * as context path, the rest will be ignored. + * @param start if set to false, server will not get started, which allows to configure the underlying transport + * layer, see above for details. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + */ + public static HttpServer createHttpServer(final URI uri, final boolean start) { + return createHttpServer(uri, (GrizzlyHttpContainer) null, false, null, start); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be + * used as context path, the rest will be ignored. + * @param configuration web application configuration. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration) { + return createHttpServer( + uri, + new GrizzlyHttpContainer(configuration), + false, + null, + true + ); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be + * used as context path, the rest will be ignored. + * @param configuration web application configuration. + * @param start if set to false, server will not get started, which allows to configure the underlying + * transport layer, see above for details. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration, final boolean start) { + return createHttpServer( + uri, + new GrizzlyHttpContainer(configuration), + false, + null, + start); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri URI on which the Jersey web application will be deployed. Only first path segment + * will be used as context path, the rest will be ignored. + * @param configuration web application configuration. + * @param secure used for call {@link NetworkListener#setSecure(boolean)}. + * @param sslEngineConfigurator Ssl settings to be passed to {@link NetworkListener#setSSLEngineConfig}. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + */ + public static HttpServer createHttpServer(final URI uri, + final ResourceConfig configuration, + final boolean secure, + final SSLEngineConfigurator sslEngineConfigurator) { + return createHttpServer( + uri, + new GrizzlyHttpContainer(configuration), + secure, + sslEngineConfigurator, + true); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri URI on which the Jersey web application will be deployed. Only first path segment + * will be used as context path, the rest will be ignored. + * @param configuration web application configuration. + * @param secure used for call {@link NetworkListener#setSecure(boolean)}. + * @param sslEngineConfigurator Ssl settings to be passed to {@link NetworkListener#setSSLEngineConfig}. + * @param start if set to false, server will not get started, which allows to configure the + * underlying transport, see above for details. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + */ + public static HttpServer createHttpServer(final URI uri, + final ResourceConfig configuration, + final boolean secure, + final SSLEngineConfigurator sslEngineConfigurator, + final boolean start) { + return createHttpServer( + uri, + new GrizzlyHttpContainer(configuration), + secure, + sslEngineConfigurator, + start); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path + * segment will be used as context path, the rest will be ignored. + * @param config web application configuration. + * @param secure used for call {@link NetworkListener#setSecure(boolean)}. + * @param sslEngineConfigurator Ssl settings to be passed to {@link NetworkListener#setSSLEngineConfig}. + * @param parentContext DI provider specific context with application's registered bindings. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + * @see GrizzlyHttpContainer + * @since 2.12 + */ + public static HttpServer createHttpServer(final URI uri, + final ResourceConfig config, + final boolean secure, + final SSLEngineConfigurator sslEngineConfigurator, + final Object parentContext) { + return createHttpServer(uri, new GrizzlyHttpContainer(config, parentContext), secure, sslEngineConfigurator, + true); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path + * segment will be used as context path, the rest will be ignored. + * @param config web application configuration. + * @param parentContext DI provider specific context with application's registered bindings. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + * @see GrizzlyHttpContainer + * @since 2.12 + */ + public static HttpServer createHttpServer(final URI uri, + final ResourceConfig config, + final Object parentContext) { + return createHttpServer(uri, new GrizzlyHttpContainer(config, parentContext), false, null, true); + } + + /** + * Create new {@link HttpServer} instance. + * + * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path + * segment will be used as context path, the rest will be ignored. + * @param handler {@link HttpHandler} instance. + * @param secure used for call {@link NetworkListener#setSecure(boolean)}. + * @param sslEngineConfigurator Ssl settings to be passed to {@link NetworkListener#setSSLEngineConfig}. + * @param start if set to false, server will not get started, this allows end users to set + * additional properties on the underlying listener. + * @return newly created {@code HttpServer}. + * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance. + * @see GrizzlyHttpContainer + */ + public static HttpServer createHttpServer(final URI uri, + final GrizzlyHttpContainer handler, + final boolean secure, + final SSLEngineConfigurator sslEngineConfigurator, + final boolean start) { + + final String host = (uri.getHost() == null) ? NetworkListener.DEFAULT_NETWORK_HOST : uri.getHost(); + final int port = (uri.getPort() == -1) + ? (secure ? Container.DEFAULT_HTTPS_PORT : Container.DEFAULT_HTTP_PORT) + : uri.getPort(); + + final NetworkListener listener = new NetworkListener("grizzly", host, port); + + listener.getTransport().getWorkerThreadPoolConfig().setThreadFactory(new ThreadFactoryBuilder() + .setNameFormat("grizzly-http-server-%d") + .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler()) + .build()); + + listener.setSecure(secure); + if (sslEngineConfigurator != null) { + listener.setSSLEngineConfig(sslEngineConfigurator); + } + + final HttpServer server = new HttpServer(); + server.addListener(listener); + + // Map the path to the processor. + final ServerConfiguration config = server.getServerConfiguration(); + if (handler != null) { + final String path = uri.getPath().replaceAll("/{2,}", "/"); + + final String contextPath = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; + config.addHttpHandler(handler, HttpHandlerRegistration.bulder().contextPath(contextPath).build()); + } + + config.setPassTraceRequest(true); + config.setDefaultQueryEncoding(Charsets.UTF8_CHARSET); + + if (start) { + try { + // Start the server. + server.start(); + } catch (final IOException ex) { + server.shutdownNow(); + throw new ProcessingException(LocalizationMessages.FAILED_TO_START_SERVER(ex.getMessage()), ex); + } + } + + return server; + } + + /** + * Prevents instantiation. + */ + private GrizzlyHttpServerFactory() { + } +}
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyRequestPropertiesDelegate.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyRequestPropertiesDelegate.java new file mode 100644 index 0000000..d14917f --- /dev/null +++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyRequestPropertiesDelegate.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly2.httpserver; + +import java.util.Collection; + +import org.glassfish.jersey.internal.PropertiesDelegate; + +import org.glassfish.grizzly.http.server.Request; + +/** + * Grizzly container {@link PropertiesDelegate properties delegate}. + * + * @author Martin Matula + */ +class GrizzlyRequestPropertiesDelegate implements PropertiesDelegate { + private final Request request; + + /** + * Create new Grizzly container properties delegate instance. + * + * @param request grizzly HTTP request. + */ + GrizzlyRequestPropertiesDelegate(Request request) { + this.request = request; + } + + @Override + public Object getProperty(String name) { + return request.getAttribute(name); + } + + @Override + public Collection<String> getPropertyNames() { + return request.getAttributeNames(); + } + + @Override + public void setProperty(String name, Object value) { + request.setAttribute(name, value); + } + + @Override + public void removeProperty(String name) { + request.removeAttribute(name); + } +}
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/package-info.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/package-info.java new file mode 100644 index 0000000..75cf389 --- /dev/null +++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Grizzly 2.x container classes. + */ +package org.glassfish.jersey.grizzly2.httpserver;
diff --git a/containers/grizzly2-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/grizzly2-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000..57f405d --- /dev/null +++ b/containers/grizzly2-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainerProvider \ No newline at end of file
diff --git a/containers/grizzly2-http/src/main/resources/org/glassfish/jersey/grizzly2/httpserver/internal/localization.properties b/containers/grizzly2-http/src/main/resources/org/glassfish/jersey/grizzly2/httpserver/internal/localization.properties new file mode 100644 index 0000000..83ce439 --- /dev/null +++ b/containers/grizzly2-http/src/main/resources/org/glassfish/jersey/grizzly2/httpserver/internal/localization.properties
@@ -0,0 +1,20 @@ +# +# Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# {0} - status code; {1} - status reason message +exception.sending.error.response=I/O exception occurred while sending "{0}/{1}" error response. +# {0} - exception message +failed.to.start.server=Failed to start Grizzly HTTP server: {0}
diff --git a/containers/grizzly2-servlet/pom.xml b/containers/grizzly2-servlet/pom.xml new file mode 100644 index 0000000..caee668 --- /dev/null +++ b/containers/grizzly2-servlet/pom.xml
@@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-grizzly2-servlet</artifactId> + <packaging>jar</packaging> + <name>jersey-container-grizzly2-servlet</name> + + <description>Grizzly 2 Servlet Container.</description> + + <dependencies> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <version>${servlet4.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-grizzly2-http</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.grizzly</groupId> + <artifactId>grizzly-http-servlet</artifactId> + </dependency> + </dependencies> + + <build> + <resources> + <resource> + <directory>${basedir}/src/main/java</directory> + <includes> + <include>META-INF/**/*</include> + </includes> + </resource> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package> + javax.servlet.*;version="[2.4,5.0)", + * + </Import-Package> + </instructions> + <unpackBundle>true</unpackBundle> + </configuration> + </plugin> + </plugins> + </build> +</project>
diff --git a/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java b/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java new file mode 100644 index 0000000..db969dd --- /dev/null +++ b/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java
@@ -0,0 +1,286 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.grizzly2.servlet; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +import javax.servlet.Servlet; + +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.uri.UriComponent; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.grizzly.servlet.ServletRegistration; +import org.glassfish.grizzly.servlet.WebappContext; + +/** + * Factory for creating and starting Grizzly 2 {@link HttpServer} instances + * for deploying a {@code Servlet}. + * <p/> + * The default deployed server is an instance of {@link ServletContainer}. + * <p/> + * If no initialization parameters are declared (or is null) then root + * resource and provider classes will be found by searching the classes + * referenced in the java classpath. + * + * @author Paul Sandoz + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public final class GrizzlyWebContainerFactory { + + private GrizzlyWebContainerFactory() { + } + + /** + * Create a {@link HttpServer} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @return the http server, with the endpoint started. + * + * @throws java.io.IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(String u) + throws IOException, IllegalArgumentException { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u)); + } + + /** + * Create a {@link HttpServer} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(String u, Map<String, String> initParams) + throws IOException, IllegalArgumentException { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u), initParams); + } + + /** + * Create a {@link HttpServer} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(URI u) + throws IOException, IllegalArgumentException { + return create(u, ServletContainer.class); + } + + /** + * Create a {@link HttpServer} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(URI u, + Map<String, String> initParams) throws IOException { + return create(u, ServletContainer.class, initParams); + } + + /** + * Create a {@link HttpServer} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(String u, Class<? extends Servlet> c) throws IOException { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u), c); + } + + /** + * Create a {@link HttpServer} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(String u, Class<? extends Servlet> c, + Map<String, String> initParams) throws IOException { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u), c, initParams); + } + + /** + * Create a {@link HttpServer} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(URI u, Class<? extends Servlet> c) throws IOException { + return create(u, c, null); + } + + /** + * Create a {@link HttpServer} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(URI u, Class<? extends Servlet> c, + Map<String, String> initParams) throws IOException { + return create(u, c, null, initParams, null); + } + + private static HttpServer create(URI u, Class<? extends Servlet> c, Servlet servlet, + Map<String, String> initParams, Map<String, String> contextInitParams) + throws IOException { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + String path = u.getPath(); + if (path == null) { + throw new IllegalArgumentException("The URI path, of the URI " + u + ", must be non-null"); + } else if (path.isEmpty()) { + throw new IllegalArgumentException("The URI path, of the URI " + u + ", must be present"); + } else if (path.charAt(0) != '/') { + throw new IllegalArgumentException("The URI path, of the URI " + u + ". must start with a '/'"); + } + + path = String.format("/%s", UriComponent.decodePath(u.getPath(), true).get(1).toString()); + + WebappContext context = new WebappContext("GrizzlyContext", path); + ServletRegistration registration; + if (c != null) { + registration = context.addServlet(c.getName(), c); + } else { + registration = context.addServlet(servlet.getClass().getName(), servlet); + } + registration.addMapping("/*"); + + if (contextInitParams != null) { + for (Map.Entry<String, String> e : contextInitParams.entrySet()) { + context.setInitParameter(e.getKey(), e.getValue()); + } + } + + if (initParams != null) { + registration.setInitParameters(initParams); + } + + HttpServer server = GrizzlyHttpServerFactory.createHttpServer(u); + context.deploy(server); + return server; + } + + /** + * Create a {@link HttpServer} that registers the declared servlet instance. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param servlet the servlet instance. + * @param initParams the servlet initialization parameters. + * @param contextInitParams the servlet context initialization parameters. + * @return the http server, with the endpoint started. + * + * @throws IOException if an error occurs creating the container. + * @throws IllegalArgumentException if {@code u} is {@code null}. + */ + public static HttpServer create(URI u, Servlet servlet, Map<String, String> initParams, Map<String, String> contextInitParams) + throws IOException { + if (servlet == null) { + throw new IllegalArgumentException("The servlet must not be null"); + } + return create(u, null, servlet, initParams, contextInitParams); + } +}
diff --git a/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/package-info.java b/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/package-info.java new file mode 100644 index 0000000..41a7e50 --- /dev/null +++ b/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Grizzly 2.x Servlet container classes. + */ +package org.glassfish.jersey.grizzly2.servlet;
diff --git a/containers/jdk-http/pom.xml b/containers/jdk-http/pom.xml new file mode 100644 index 0000000..41c87bd --- /dev/null +++ b/containers/jdk-http/pom.xml
@@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-jdk-http</artifactId> + <packaging>jar</packaging> + <name>jersey-container-jdk-http</name> + + <description>JDK Http Container</description> + + <dependencies> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + </plugin> + </plugins> + + <resources> + <resource> + <directory>${basedir}/src/main/java</directory> + <includes> + <include>META-INF/**/*</include> + </includes> + </resource> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> + + <profiles> + <profile> + <id>windows</id> + <activation> + <os> + <family>windows</family> + </os> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- Exclude unit tests regarding JDK HTTP Server because of failing a server shutdown in a class + with several tests. "java.net.BindException: Address already in use: bind" + bug reported on https://bugs.openjdk.java.net/browse/JDK-8015692 --> + <excludes> + <exclude>org/glassfish/jersey/jdkhttp/BasicJdkHttpServerTest.java</exclude> + <exclude>org/glassfish/jersey/jdkhttp/JdkHttpPackageTest.java</exclude> + <exclude>org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java</exclude> + <exclude>org/glassfish/jersey/jdkhttp/LifecycleListenerTest.java</exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + +</project>
diff --git a/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpHandlerContainer.java b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpHandlerContainer.java new file mode 100644 index 0000000..051c5ca --- /dev/null +++ b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpHandlerContainer.java
@@ -0,0 +1,351 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.jdkhttp.internal.LocalizationMessages; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsExchange; + +/** + * Jersey {@code Container} implementation based on Java SE {@link HttpServer}. + * + * @author Miroslav Fuksa + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JdkHttpHandlerContainer implements HttpHandler, Container { + + private static final Logger LOGGER = Logger.getLogger(JdkHttpHandlerContainer.class.getName()); + + private volatile ApplicationHandler appHandler; + + /** + * Create new lightweight Java SE HTTP server container. + * + * @param application JAX-RS / Jersey application to be deployed on the container. + */ + JdkHttpHandlerContainer(final Application application) { + this.appHandler = new ApplicationHandler(application); + } + + /** + * Create new lightweight Java SE HTTP server container. + * + * @param application JAX-RS / Jersey application to be deployed on the container. + * @param parentContext DI provider specific context with application's registered bindings. + */ + JdkHttpHandlerContainer(final Application application, final Object parentContext) { + this.appHandler = new ApplicationHandler(application, null, parentContext); + } + + @Override + public void handle(final HttpExchange exchange) throws IOException { + /** + * This is a URI that contains the path, query and fragment components. + */ + URI exchangeUri = exchange.getRequestURI(); + + /** + * The base path specified by the HTTP context of the HTTP handler. It + * is in decoded form. + */ + String decodedBasePath = exchange.getHttpContext().getPath(); + + // Ensure that the base path ends with a '/' + if (!decodedBasePath.endsWith("/")) { + if (decodedBasePath.equals(exchangeUri.getPath())) { + /** + * This is an edge case where the request path does not end in a + * '/' and is equal to the context path of the HTTP handler. + * Both the request path and base path need to end in a '/' + * Currently the request path is modified. + * + * TODO support redirection in accordance with resource configuration feature. + */ + exchangeUri = UriBuilder.fromUri(exchangeUri) + .path("/").build(); + } + decodedBasePath += "/"; + } + + /* + * The following is madness, there is no easy way to get the complete + * URI of the HTTP request!! + * + * TODO this is missing the user information component, how can this be obtained? + */ + final boolean isSecure = exchange instanceof HttpsExchange; + final String scheme = isSecure ? "https" : "http"; + + final URI baseUri = getBaseUri(exchange, decodedBasePath, scheme); + final URI requestUri = getRequestUri(exchange, baseUri); + + final ResponseWriter responseWriter = new ResponseWriter(exchange); + final ContainerRequest requestContext = new ContainerRequest(baseUri, requestUri, + exchange.getRequestMethod(), getSecurityContext(exchange.getPrincipal(), isSecure), + new MapPropertiesDelegate()); + requestContext.setEntityStream(exchange.getRequestBody()); + requestContext.getHeaders().putAll(exchange.getRequestHeaders()); + requestContext.setWriter(responseWriter); + try { + appHandler.handle(requestContext); + } finally { + // if the response was not committed yet by the JerseyApplication + // then commit it and log warning + responseWriter.closeAndLogWarning(); + } + } + + private URI getBaseUri(final HttpExchange exchange, final String decodedBasePath, final String scheme) { + final URI baseUri; + try { + final List<String> hostHeader = exchange.getRequestHeaders().get("Host"); + if (hostHeader != null) { + baseUri = new URI(scheme + "://" + hostHeader.get(0) + decodedBasePath); + } else { + final InetSocketAddress addr = exchange.getLocalAddress(); + baseUri = new URI(scheme, null, addr.getHostName(), addr.getPort(), + decodedBasePath, null, null); + } + } catch (final URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + return baseUri; + } + + private URI getRequestUri(final HttpExchange exchange, final URI baseUri) { + try { + return new URI(getServerAddress(baseUri) + exchange.getRequestURI()); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private String getServerAddress(final URI baseUri) throws URISyntaxException { + return new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), null, null, null).toString(); + } + + private SecurityContext getSecurityContext(final Principal principal, final boolean isSecure) { + return new SecurityContext() { + + @Override + public boolean isUserInRole(final String role) { + return false; + } + + @Override + public boolean isSecure() { + return isSecure; + } + + @Override + public Principal getUserPrincipal() { + return principal; + } + + @Override + public String getAuthenticationScheme() { + return null; + } + }; + } + + @Override + public ResourceConfig getConfiguration() { + return appHandler.getConfiguration(); + } + + @Override + public void reload() { + reload(getConfiguration()); + } + + @Override + public void reload(final ResourceConfig configuration) { + appHandler.onShutdown(this); + + appHandler = new ApplicationHandler(configuration); + appHandler.onReload(this); + appHandler.onStartup(this); + } + + @Override + public ApplicationHandler getApplicationHandler() { + return appHandler; + } + + /** + * Inform this container that the server has been started. + * + * This method must be implicitly called after the server containing this container is started. + */ + void onServerStart() { + this.appHandler.onStartup(this); + } + + /** + * Inform this container that the server is being stopped. + * + * This method must be implicitly called before the server containing this container is stopped. + */ + void onServerStop() { + this.appHandler.onShutdown(this); + } + + private static final class ResponseWriter implements ContainerResponseWriter { + + private final HttpExchange exchange; + private final AtomicBoolean closed; + + /** + * Creates a new ResponseWriter for given {@link HttpExchange HTTP Exchange}. + * + * @param exchange Exchange of the {@link HttpServer JDK Http Server} + */ + ResponseWriter(final HttpExchange exchange) { + this.exchange = exchange; + this.closed = new AtomicBoolean(false); + } + + @Override + public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context) + throws ContainerException { + final MultivaluedMap<String, String> responseHeaders = context.getStringHeaders(); + final Headers serverHeaders = exchange.getResponseHeaders(); + for (final Map.Entry<String, List<String>> e : responseHeaders.entrySet()) { + for (final String value : e.getValue()) { + serverHeaders.add(e.getKey(), value); + } + } + + try { + if (context.getStatus() == Response.Status.NO_CONTENT.getStatusCode()) { + // Work around bug in LW HTTP server + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6886436 + exchange.sendResponseHeaders(context.getStatus(), -1); + } else { + exchange.sendResponseHeaders(context.getStatus(), + getResponseLength(contentLength)); + } + } catch (final IOException ioe) { + throw new ContainerException(LocalizationMessages.ERROR_RESPONSEWRITER_WRITING_HEADERS(), ioe); + } + + return exchange.getResponseBody(); + } + + private long getResponseLength(final long contentLength) { + if (contentLength == 0) { + return -1; + } + if (contentLength < 0) { + return 0; + } + return contentLength; + } + + @Override + public boolean suspend(final long timeOut, final TimeUnit timeUnit, final TimeoutHandler timeoutHandler) { + throw new UnsupportedOperationException("Method suspend is not supported by the container."); + } + + @Override + public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throws IllegalStateException { + throw new UnsupportedOperationException("Method setSuspendTimeout is not supported by the container."); + } + + @Override + public void failure(final Throwable error) { + try { + exchange.sendResponseHeaders(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), getResponseLength(0)); + } catch (final IOException e) { + LOGGER.log(Level.WARNING, LocalizationMessages.ERROR_RESPONSEWRITER_SENDING_FAILURE_RESPONSE(), e); + } finally { + commit(); + rethrow(error); + } + } + + @Override + public boolean enableResponseBuffering() { + return true; + } + + @Override + public void commit() { + if (closed.compareAndSet(false, true)) { + exchange.close(); + } + } + + /** + * Rethrow the original exception as required by JAX-RS, 3.3.4 + * + * @param error throwable to be re-thrown + */ + private void rethrow(final Throwable error) { + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new ContainerException(error); + } + } + + /** + * Commits the response and logs a warning message. + * + * This method should be called by the container at the end of the + * handle method to make sure that the ResponseWriter was committed. + */ + private void closeAndLogWarning() { + if (closed.compareAndSet(false, true)) { + exchange.close(); + LOGGER.log(Level.WARNING, LocalizationMessages.ERROR_RESPONSEWRITER_RESPONSE_UNCOMMITED()); + } + } + } +}
diff --git a/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpHandlerContainerProvider.java b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpHandlerContainerProvider.java new file mode 100644 index 0000000..cd6306d --- /dev/null +++ b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpHandlerContainerProvider.java
@@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.server.spi.ContainerProvider; + +import com.sun.net.httpserver.HttpHandler; + +/** + * Container provider for containers based on lightweight Java SE HTTP Server's {@link HttpHandler}. + * + * @author Miroslav Fuksa + */ +public final class JdkHttpHandlerContainerProvider implements ContainerProvider { + + @Override + public <T> T createContainer(Class<T> type, Application application) throws ProcessingException { + if (type != HttpHandler.class && type != JdkHttpHandlerContainer.class) { + return null; + } + return type.cast(new JdkHttpHandlerContainer(application)); + } +}
diff --git a/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServerFactory.java b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServerFactory.java new file mode 100644 index 0000000..0f1fbb2 --- /dev/null +++ b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServerFactory.java
@@ -0,0 +1,386 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder; +import org.glassfish.jersey.jdkhttp.internal.LocalizationMessages; +import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; + +/** + * Factory for creating {@link HttpServer JDK HttpServer} instances to run Jersey applications. + * + * @author Miroslav Fuksa + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class JdkHttpServerFactory { + + private static final Logger LOG = Logger.getLogger(JdkHttpServerFactory.class.getName()); + + /** + * Create and start the {@link HttpServer JDK HttpServer} with the Jersey application deployed + * at the given {@link URI}. + * <p> + * The returned {@link HttpServer JDK HttpServer} is started. + * </p> + * + * @param uri the {@link URI uri} on which the Jersey application will be deployed. + * @param configuration the Jersey server-side application configuration. + * @return Newly created {@link HttpServer}. + * @throws ProcessingException thrown when problems during server creation + * occurs. + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration) { + return createHttpServer(uri, configuration, true); + } + + /** + * Create (and possibly start) the {@link HttpServer JDK HttpServer} with the JAX-RS / Jersey application deployed + * on the given {@link URI}. + * <p> + * The {@code start} flag controls whether or not the returned {@link HttpServer JDK HttpServer} is started. + * </p> + * + * @param uri the {@link URI uri} on which the Jersey application will be deployed. + * @param configuration the Jersey server-side application configuration. + * @param start if set to {@code false}, the created server will not be automatically started. + * @return Newly created {@link HttpServer}. + * @throws ProcessingException thrown when problems during server creation occurs. + * @since 2.8 + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration, final boolean start) { + return createHttpServer(uri, new JdkHttpHandlerContainer(configuration), start); + } + + /** + * Create (and possibly start) the {@link HttpServer JDK HttpServer} with the JAX-RS / Jersey application deployed + * on the given {@link URI}. + * <p/> + * + * @param uri the {@link URI uri} on which the Jersey application will be deployed. + * @param configuration the Jersey server-side application configuration. + * @param parentContext DI provider specific context with application's registered bindings. + * @return Newly created {@link HttpServer}. + * @throws ProcessingException thrown when problems during server creation occurs. + * @see org.glassfish.jersey.jdkhttp.JdkHttpHandlerContainer + * @since 2.12 + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration, + final Object parentContext) { + return createHttpServer(uri, new JdkHttpHandlerContainer(configuration, parentContext), true); + } + + /** + * Create and start the {@link HttpServer JDK HttpServer}, eventually {@code HttpServer}'s subclass + * {@link HttpsServer JDK HttpsServer} with the JAX-RS / Jersey application deployed on the given {@link URI}. + * <p> + * The returned {@link HttpServer JDK HttpServer} is started. + * </p> + * + * @param uri the {@link URI uri} on which the Jersey application will be deployed. + * @param configuration the Jersey server-side application configuration. + * @param sslContext custom {@link SSLContext} to be passed to the server + * @return Newly created {@link HttpServer}. + * @throws ProcessingException thrown when problems during server creation occurs. + * @since 2.18 + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration, + final SSLContext sslContext) { + return createHttpServer(uri, new JdkHttpHandlerContainer(configuration), + sslContext, true); + } + + /** + * Create (and possibly start) the {@link HttpServer JDK HttpServer}, eventually {@code HttpServer}'s subclass + * {@link HttpsServer JDK HttpsServer} with the JAX-RS / Jersey application deployed on the given {@link URI}. + * <p> + * The {@code start} flag controls whether or not the returned {@link HttpServer JDK HttpServer} is started. + * </p> + * + * @param uri the {@link URI uri} on which the Jersey application will be deployed. + * @param configuration the Jersey server-side application configuration. + * @param sslContext custom {@link SSLContext} to be passed to the server + * @param start if set to {@code false}, the created server will not be automatically started. + * @return Newly created {@link HttpServer}. + * @throws ProcessingException thrown when problems during server creation occurs. + * @since 2.17 + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration, + final SSLContext sslContext, final boolean start) { + return createHttpServer(uri, + new JdkHttpHandlerContainer(configuration), + sslContext, + start); + } + + /** + * Create (and possibly start) the {@link HttpServer JDK HttpServer}, eventually {@code HttpServer}'s subclass + * {@link HttpsServer} with the JAX-RS / Jersey application deployed on the given {@link URI}. + * <p> + * The {@code start} flag controls whether or not the returned {@link HttpServer JDK HttpServer} is started. + * </p> + * + * @param uri the {@link URI uri} on which the Jersey application will be deployed. + * @param configuration the Jersey server-side application configuration. + * @param parentContext DI provider specific context with application's registered bindings. + * @param sslContext custom {@link SSLContext} to be passed to the server + * @param start if set to {@code false}, the created server will not be automatically started. + * @return Newly created {@link HttpServer}. + * @throws ProcessingException thrown when problems during server creation occurs. + * @since 2.18 + */ + public static HttpServer createHttpServer(final URI uri, final ResourceConfig configuration, + final Object parentContext, + final SSLContext sslContext, final boolean start) { + return createHttpServer(uri, + new JdkHttpHandlerContainer(configuration, parentContext), + sslContext, + start + ); + } + + private static HttpServer createHttpServer(final URI uri, final JdkHttpHandlerContainer handler, + final boolean start) { + return createHttpServer(uri, handler, null, start); + } + + private static HttpServer createHttpServer(final URI uri, + final JdkHttpHandlerContainer handler, + final SSLContext sslContext, + final boolean start) { + if (uri == null) { + throw new IllegalArgumentException(LocalizationMessages.ERROR_CONTAINER_URI_NULL()); + } + + final String scheme = uri.getScheme(); + final boolean isHttp = "http".equalsIgnoreCase(scheme); + final boolean isHttps = "https".equalsIgnoreCase(scheme); + final HttpsConfigurator httpsConfigurator = sslContext != null ? new HttpsConfigurator(sslContext) : null; + + if (isHttp) { + if (httpsConfigurator != null) { + // attempt to use https with http scheme + LOG.warning(LocalizationMessages.WARNING_CONTAINER_URI_SCHEME_SECURED()); + } + } else if (isHttps) { + if (httpsConfigurator == null) { + if (start) { + // The SSLContext (via HttpsConfigurator) has to be set before the server starts. + // Starting https server w/o SSL is invalid, it will lead to error anyway. + throw new IllegalArgumentException(LocalizationMessages.ERROR_CONTAINER_HTTPS_NO_SSL()); + } else { + // Creating the https server w/o SSL context, but not starting it is valid. + // However, server.setHttpsConfigurator() must be called before the start. + LOG.info(LocalizationMessages.INFO_CONTAINER_HTTPS_NO_SSL()); + } + } + } else { + throw new IllegalArgumentException(LocalizationMessages.ERROR_CONTAINER_URI_SCHEME_UNKNOWN(uri)); + } + + final String path = uri.getPath(); + if (path == null) { + throw new IllegalArgumentException(LocalizationMessages.ERROR_CONTAINER_URI_PATH_NULL(uri)); + } else if (path.isEmpty()) { + throw new IllegalArgumentException(LocalizationMessages.ERROR_CONTAINER_URI_PATH_EMPTY(uri)); + } else if (path.charAt(0) != '/') { + throw new IllegalArgumentException(LocalizationMessages.ERROR_CONTAINER_URI_PATH_START(uri)); + } + + final int port = (uri.getPort() == -1) + ? (isHttp ? Container.DEFAULT_HTTP_PORT : Container.DEFAULT_HTTPS_PORT) + : uri.getPort(); + + final HttpServer server; + try { + server = isHttp + ? HttpServer.create(new InetSocketAddress(port), 0) + : HttpsServer.create(new InetSocketAddress(port), 0); + } catch (final IOException ioe) { + throw new ProcessingException(LocalizationMessages.ERROR_CONTAINER_EXCEPTION_IO(), ioe); + } + + if (isHttps && httpsConfigurator != null) { + ((HttpsServer) server).setHttpsConfigurator(httpsConfigurator); + } + + server.setExecutor(Executors.newCachedThreadPool(new ThreadFactoryBuilder() + .setNameFormat("jdk-http-server-%d") + .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler()) + .build())); + server.createContext(path, handler); + + final HttpServer wrapper = isHttp + ? createHttpServerWrapper(server, handler) + : createHttpsServerWrapper((HttpsServer) server, handler); + + if (start) { + wrapper.start(); + } + + return wrapper; + } + + private static HttpServer createHttpsServerWrapper(final HttpsServer delegate, final JdkHttpHandlerContainer handler) { + return new HttpsServer() { + + @Override + public void setHttpsConfigurator(final HttpsConfigurator httpsConfigurator) { + delegate.setHttpsConfigurator(httpsConfigurator); + } + + @Override + public HttpsConfigurator getHttpsConfigurator() { + return delegate.getHttpsConfigurator(); + } + + @Override + public void bind(final InetSocketAddress inetSocketAddress, final int i) throws IOException { + delegate.bind(inetSocketAddress, i); + } + + @Override + public void start() { + delegate.start(); + handler.onServerStart(); + } + + @Override + public void setExecutor(final Executor executor) { + delegate.setExecutor(executor); + } + + @Override + public Executor getExecutor() { + return delegate.getExecutor(); + } + + @Override + public void stop(final int i) { + handler.onServerStop(); + delegate.stop(i); + } + + @Override + public HttpContext createContext(final String s, final HttpHandler httpHandler) { + return delegate.createContext(s, httpHandler); + } + + @Override + public HttpContext createContext(final String s) { + return delegate.createContext(s); + } + + @Override + public void removeContext(final String s) throws IllegalArgumentException { + delegate.removeContext(s); + } + + @Override + public void removeContext(final HttpContext httpContext) { + delegate.removeContext(httpContext); + } + + @Override + public InetSocketAddress getAddress() { + return delegate.getAddress(); + } + }; + } + + private static HttpServer createHttpServerWrapper(final HttpServer delegate, final JdkHttpHandlerContainer handler) { + return new HttpServer() { + + @Override + public void bind(final InetSocketAddress inetSocketAddress, final int i) throws IOException { + delegate.bind(inetSocketAddress, i); + } + + @Override + public void start() { + delegate.start(); + handler.onServerStart(); + } + + @Override + public void setExecutor(final Executor executor) { + delegate.setExecutor(executor); + } + + @Override + public Executor getExecutor() { + return delegate.getExecutor(); + } + + @Override + public void stop(final int i) { + handler.onServerStop(); + delegate.stop(i); + } + + @Override + public HttpContext createContext(final String s, final HttpHandler httpHandler) { + return delegate.createContext(s, httpHandler); + } + + @Override + public HttpContext createContext(final String s) { + return delegate.createContext(s); + } + + @Override + public void removeContext(final String s) throws IllegalArgumentException { + delegate.removeContext(s); + } + + @Override + public void removeContext(final HttpContext httpContext) { + delegate.removeContext(httpContext); + } + + @Override + public InetSocketAddress getAddress() { + return delegate.getAddress(); + } + }; + } + + /** + * Prevents instantiation. + */ + private JdkHttpServerFactory() { + throw new AssertionError("Instantiation not allowed."); + } +}
diff --git a/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/package-info.java b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/package-info.java new file mode 100644 index 0000000..c60ec6e --- /dev/null +++ b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/package-info.java
@@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * The container adapter between {@link com.sun.net.httpserver.HttpServer JDK HTTP server} + * and Jersey {@link org.glassfish.jersey.server.ApplicationHandler Jersey application handler} + * classes. + */ +package org.glassfish.jersey.jdkhttp;
diff --git a/containers/jdk-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/jdk-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000..efdf94e --- /dev/null +++ b/containers/jdk-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.jdkhttp.JdkHttpHandlerContainerProvider \ No newline at end of file
diff --git a/containers/jdk-http/src/main/resources/org/glassfish/jersey/jdkhttp/internal/localization.properties b/containers/jdk-http/src/main/resources/org/glassfish/jersey/jdkhttp/internal/localization.properties new file mode 100644 index 0000000..745c494 --- /dev/null +++ b/containers/jdk-http/src/main/resources/org/glassfish/jersey/jdkhttp/internal/localization.properties
@@ -0,0 +1,30 @@ +# +# Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +error.container.exception.io=IOException thrown when creating the JDK HttpServer. +error.container.uri.null=The URI must not be null. +error.container.uri.path.empty=The URI path, of the URI {0} must be present (not an empty string). +error.container.uri.path.null=The URI path, of the URI {0} must be non-null. +error.container.uri.path.start=The URI path, of the URI {0} must start with a '/'. +error.container.uri.scheme.unknown=The URI scheme, of the URI {0} must be equal (ignoring case) to 'http' or 'https'. +error.container.https.no.ssl=Attempt to start a HTTPS server with no SSL context defined. +error.responsewriter.response.uncommited=ResponseWriter was not commited yet. Committing the Response now. +error.responsewriter.sending.failure.response=Unable to send a failure response. +error.responsewriter.writing.headers=Error writing out the response headers. +info.container.https.no.ssl=HTTPS server will be created with no SSL context defined. HttpsConfigurator must be \ + set before the server is started. +warning.container.uri.scheme.secured=SSLContext is set, but http scheme was used instead of https. The SSLContext will \ + be ignored.
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/AbstractJdkHttpServerTester.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/AbstractJdkHttpServerTester.java new file mode 100644 index 0000000..24998f9 --- /dev/null +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/AbstractJdkHttpServerTester.java
@@ -0,0 +1,112 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import java.net.URI; +import java.security.AccessController; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.UriBuilder; + +import com.sun.net.httpserver.HttpServer; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.After; + +/** + * Abstract JDK HTTP Server unit tester. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public abstract class AbstractJdkHttpServerTester { + + public static final String CONTEXT = ""; + private final int DEFAULT_PORT = 9998; + + private static final Logger LOGGER = Logger.getLogger(AbstractJdkHttpServerTester.class.getName()); + + /** + * Get the port to be used for test application deployments. + * + * @return The HTTP port of the URI + */ + protected final int getPort() { + final String value = + AccessController.doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port")); + if (value != null) { + + try { + final int i = Integer.parseInt(value); + if (i <= 0) { + throw new NumberFormatException("Value not positive."); + } + return i; + } catch (NumberFormatException e) { + LOGGER.log(Level.CONFIG, + "Value of 'jersey.config.test.container.port'" + + " property is not a valid positive integer [" + value + "]." + + " Reverting to default [" + DEFAULT_PORT + "].", + e); + } + } + return DEFAULT_PORT; + } + + private volatile HttpServer server; + + public UriBuilder getUri() { + return UriBuilder.fromUri("http://localhost").port(getPort()).path(CONTEXT); + } + + public void startServer(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + config.register(LoggingFeature.class); + final URI baseUri = getBaseUri(); + server = JdkHttpServerFactory.createHttpServer(baseUri, config); + LOGGER.log(Level.INFO, "jdk-http server started on base uri: " + baseUri); + } + + public void startServer(ResourceConfig config) { + final URI baseUri = getBaseUri(); + config.register(LoggingFeature.class); + server = JdkHttpServerFactory.createHttpServer(baseUri, config); + LOGGER.log(Level.INFO, "jdk-http server started on base uri: " + baseUri); + } + + public URI getBaseUri() { + return UriBuilder.fromUri("http://localhost/").port(getPort()).build(); + } + + public void stopServer() { + try { + server.stop(3); + server = null; + LOGGER.log(Level.INFO, "Simple-http server stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @After + public void tearDown() { + if (server != null) { + stopServer(); + } + } +}
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/BasicJdkHttpServerTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/BasicJdkHttpServerTest.java new file mode 100644 index 0000000..62a344d --- /dev/null +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/BasicJdkHttpServerTest.java
@@ -0,0 +1,78 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.junit.After; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsServer; + +/** + * Jdk Http Server basic tests. + * + * @author Michal Gajdos + */ +public class BasicJdkHttpServerTest extends AbstractJdkHttpServerTester { + + private HttpServer server; + + @Path("/test") + public static class TestResource { + + @GET + public String get() { + return "test"; + } + } + + @Test + public void testCreateHttpServer() throws Exception { + server = JdkHttpServerFactory.createHttpServer( + UriBuilder.fromUri("http://localhost/").port(getPort()).build(), new ResourceConfig(TestResource.class)); + + assertThat(server, instanceOf(HttpServer.class)); + assertThat(server, not(instanceOf(HttpsServer.class))); + } + + @Test + public void testCreateHttpsServer() throws Exception { + server = JdkHttpServerFactory.createHttpServer( + UriBuilder.fromUri("https://localhost/").port(getPort()).build(), + new ResourceConfig(TestResource.class), + false); + + assertThat(server, instanceOf(HttpsServer.class)); + } + + @After + public void tearDown() { + if (server != null) { + server.stop(3); + server = null; + } + } +}
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpPackageTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpPackageTest.java new file mode 100644 index 0000000..675bbaa --- /dev/null +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpPackageTest.java
@@ -0,0 +1,65 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Jdk Http Container package scanning test. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JdkHttpPackageTest extends AbstractJdkHttpServerTester { + + @Path("/packageTest") + public static class TestResource { + @GET + public String get() { + return "test"; + } + + @GET + @Path("sub") + public String getSub() { + return "test-sub"; + } + } + + @Test + public void testJdkHttpPackage() { + final ResourceConfig rc = new ResourceConfig(); + rc.packages(this.getClass().getPackage().getName()); + + startServer(rc); + + WebTarget r = ClientBuilder.newClient().target(getUri().path("/").build()); + + assertEquals("test", r.path("packageTest").request().get(String.class)); + assertEquals("test-sub", r.path("packageTest/sub").request().get(String.class)); + assertEquals(404, r.path("wrong").request().get(Response.class).getStatus()); + + } +}
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java new file mode 100644 index 0000000..180f2bc --- /dev/null +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
@@ -0,0 +1,214 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.UriBuilder; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.server.ResourceConfig; + +import org.junit.After; +import org.junit.Test; + +import com.google.common.io.ByteStreams; + +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +/** + * Jdk Https Server tests. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +public class JdkHttpsServerTest extends AbstractJdkHttpServerTester { + + private static final String TRUSTSTORE_CLIENT_FILE = "./truststore_client"; + private static final String TRUSTSTORE_CLIENT_PWD = "asdfgh"; + private static final String KEYSTORE_CLIENT_FILE = "./keystore_client"; + private static final String KEYSTORE_CLIENT_PWD = "asdfgh"; + + private static final String KEYSTORE_SERVER_FILE = "./keystore_server"; + private static final String KEYSTORE_SERVER_PWD = "asdfgh"; + private static final String TRUSTSTORE_SERVER_FILE = "./truststore_server"; + private static final String TRUSTSTORE_SERVER_PWD = "asdfgh"; + + private HttpServer server; + private final URI httpsUri = UriBuilder.fromUri("https://localhost/").port(getPort()).build(); + private final URI httpUri = UriBuilder.fromUri("http://localhost/").port(getPort()).build(); + private final ResourceConfig rc = new ResourceConfig(TestResource.class); + + @Path("/testHttps") + public static class TestResource { + @GET + public String get() { + return "test"; + } + } + + /** + * Test, that {@link HttpsServer} instance is returned when providing empty SSLContext (but not starting). + * @throws Exception + */ + @Test + public void testCreateHttpsServerNoSslContext() throws Exception { + server = JdkHttpServerFactory.createHttpServer(httpsUri, rc, null, false); + assertThat(server, instanceOf(HttpsServer.class)); + } + + /** + * Test, that exception is thrown when attempting to start a {@link HttpsServer} with empty SSLContext. + * @throws Exception + */ + @Test(expected = IllegalArgumentException.class) + public void testStartHttpServerNoSslContext() throws Exception { + server = JdkHttpServerFactory.createHttpServer(httpsUri, rc, null, true); + } + + /** + * Test, that {@link javax.net.ssl.SSLHandshakeException} is thrown when attepmting to connect to server with client + * not configured correctly. + * @throws Exception + */ + @Test(expected = SSLHandshakeException.class) + public void testCreateHttpsServerDefaultSslContext() throws Throwable { + server = JdkHttpServerFactory.createHttpServer(httpsUri, rc, SSLContext.getDefault(), true); + assertThat(server, instanceOf(HttpsServer.class)); + + // access the https server with not configured client + final Client client = ClientBuilder.newBuilder().newClient(); + try { + client.target(httpsUri).path("testHttps").request().get(String.class); + } catch (final ProcessingException e) { + throw e.getCause(); + } + } + + /** + * Test, that {@link HttpsServer} can be manually started even with (empty) SSLContext, but will throw an exception + * on request. + * @throws Exception + */ + @Test(expected = IOException.class) + public void testHttpsServerNoSslContextDelayedStart() throws Throwable { + server = JdkHttpServerFactory.createHttpServer(httpsUri, rc, null, false); + assertThat(server, instanceOf(HttpsServer.class)); + server.start(); + + final Client client = ClientBuilder.newBuilder().newClient(); + try { + client.target(httpsUri).path("testHttps").request().get(String.class); + } catch (final ProcessingException e) { + throw e.getCause(); + } + } + + /** + * Test, that {@link HttpsServer} cannot be configured with {@link HttpsConfigurator} after it has started. + * @throws Exception + */ + @Test(expected = IllegalStateException.class) + public void testConfigureSslContextAfterStart() throws Throwable { + server = JdkHttpServerFactory.createHttpServer(httpsUri, rc, null, false); + assertThat(server, instanceOf(HttpsServer.class)); + server.start(); + ((HttpsServer) server).setHttpsConfigurator(new HttpsConfigurator(getServerSslContext())); + } + + /** + * Tests a client to server roundtrip with correctly configured SSL on both sides. + * @throws IOException + */ + @Test + public void testCreateHttpsServerRoundTrip() throws IOException { + final SSLContext serverSslContext = getServerSslContext(); + + server = JdkHttpServerFactory.createHttpServer(httpsUri, rc, serverSslContext, true); + + final SSLContext foundContext = ((HttpsServer) server).getHttpsConfigurator().getSSLContext(); + assertEquals(serverSslContext, foundContext); + + final SSLContext clientSslContext = getClientSslContext(); + final Client client = ClientBuilder.newBuilder().sslContext(clientSslContext).build(); + final String response = client.target(httpsUri).path("testHttps").request().get(String.class); + + assertEquals("test", response); + } + + /** + * Test, that if URI uses http scheme instead of https, SSLContext is ignored. + * @throws IOException + */ + @Test + public void testHttpWithSsl() throws IOException { + server = JdkHttpServerFactory.createHttpServer(httpUri, rc, getServerSslContext(), true); + assertThat(server, instanceOf(HttpServer.class)); + assertThat(server, not(instanceOf(HttpsServer.class))); + } + + private SSLContext getClientSslContext() throws IOException { + final InputStream trustStore = JdkHttpsServerTest.class.getResourceAsStream(TRUSTSTORE_CLIENT_FILE); + final InputStream keyStore = JdkHttpsServerTest.class.getResourceAsStream(KEYSTORE_CLIENT_FILE); + + + final SslConfigurator sslConfigClient = SslConfigurator.newInstance() + .trustStoreBytes(ByteStreams.toByteArray(trustStore)) + .trustStorePassword(TRUSTSTORE_CLIENT_PWD) + .keyStoreBytes(ByteStreams.toByteArray(keyStore)) + .keyPassword(KEYSTORE_CLIENT_PWD); + + return sslConfigClient.createSSLContext(); + } + + private SSLContext getServerSslContext() throws IOException { + final InputStream trustStore = JdkHttpsServerTest.class.getResourceAsStream(TRUSTSTORE_SERVER_FILE); + final InputStream keyStore = JdkHttpsServerTest.class.getResourceAsStream(KEYSTORE_SERVER_FILE); + + final SslConfigurator sslConfigServer = SslConfigurator.newInstance() + .keyStoreBytes(ByteStreams.toByteArray(keyStore)) + .keyPassword(KEYSTORE_SERVER_PWD) + .trustStoreBytes(ByteStreams.toByteArray(trustStore)) + .trustStorePassword(TRUSTSTORE_SERVER_PWD); + + return sslConfigServer.createSSLContext(); + } + + @After + public void tearDown() { + if (server != null) { + server.stop(0); + server = null; + } + } +}
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/LifecycleListenerTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/LifecycleListenerTest.java new file mode 100644 index 0000000..229058f --- /dev/null +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/LifecycleListenerTest.java
@@ -0,0 +1,135 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener; +import org.glassfish.jersey.server.spi.Container; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Reload and ContainerLifecycleListener support test. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class LifecycleListenerTest extends AbstractJdkHttpServerTester { + + @Path("/one") + public static class One { + @GET + public String get() { + return "one"; + } + + @GET + @Path("sub") + public String getSub() { + return "one-sub"; + } + } + + @Path("/two") + public static class Two { + @GET + public String get() { + return "two"; + } + } + + public static class Reloader extends AbstractContainerLifecycleListener { + Container container; + + public void reload(ResourceConfig newConfig) { + container.reload(newConfig); + } + + public void reload() { + container.reload(); + } + + @Override + public void onStartup(Container container) { + this.container = container; + } + } + + @Test + public void testReload() { + final ResourceConfig rc = new ResourceConfig(One.class); + + Reloader reloader = new Reloader(); + rc.registerInstances(reloader); + + startServer(rc); + + WebTarget r = ClientBuilder.newClient().target(getUri().path("/").build()); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals("one-sub", r.path("one/sub").request().get(String.class)); + assertEquals(404, r.path("two").request().get(Response.class).getStatus()); + + // add Two resource + reloader.reload(new ResourceConfig(One.class, Two.class)); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals("one-sub", r.path("one/sub").request().get(String.class)); + assertEquals("two", r.path("two").request().get(String.class)); + } + + static class StartStopListener extends AbstractContainerLifecycleListener { + volatile boolean started; + volatile boolean stopped; + + @Override + public void onStartup(Container container) { + started = true; + } + + @Override + public void onShutdown(Container container) { + stopped = true; + } + } + + @Test + public void testStartupShutdownHooks() { + final StartStopListener listener = new StartStopListener(); + + startServer(new ResourceConfig(One.class).register(listener)); + + WebTarget r = ClientBuilder.newClient().target(getUri().path("/").build()); + + assertThat(r.path("one").request().get(String.class), equalTo("one")); + assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404)); + + stopServer(); + + assertTrue("ContainerLifecycleListener.onStartup has not been called.", listener.started); + assertTrue("ContainerLifecycleListener.onShutdown has not been called.", listener.stopped); + } +}
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/RuntimeDelegateTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/RuntimeDelegateTest.java new file mode 100644 index 0000000..26abd46 --- /dev/null +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/RuntimeDelegateTest.java
@@ -0,0 +1,77 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jdkhttp; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.ext.RuntimeDelegate; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +/** + * @author Michal Gajdos + */ +public class RuntimeDelegateTest { + + @Path("/") + public static class Resource { + + @GET + public String get() { + return "get"; + } + } + + @Test + public void testFetch() throws Exception { + final HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + final HttpHandler handler = RuntimeDelegate.getInstance().createEndpoint(new Application() { + + @Override + public Set<Class<?>> getClasses() { + return Collections.<Class<?>>singleton(Resource.class); + } + }, HttpHandler.class); + + try { + server.createContext("/", handler); + server.start(); + + final Response response = ClientBuilder.newClient() + .target(UriBuilder.fromUri("http://localhost/").port(server.getAddress().getPort()).build()) + .request() + .get(); + + assertThat(response.readEntity(String.class), is("get")); + } finally { + server.stop(0); + } + } +}
diff --git a/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/keystore_client b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/keystore_client new file mode 100644 index 0000000..d016fd2 --- /dev/null +++ b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/keystore_client Binary files differ
diff --git a/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/keystore_server b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/keystore_server new file mode 100644 index 0000000..a7c93fc --- /dev/null +++ b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/keystore_server Binary files differ
diff --git a/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/truststore_client b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/truststore_client new file mode 100644 index 0000000..74784fb --- /dev/null +++ b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/truststore_client Binary files differ
diff --git a/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/truststore_server b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/truststore_server new file mode 100644 index 0000000..9b26ce4 --- /dev/null +++ b/containers/jdk-http/src/test/resources/org/glassfish/jersey/jdkhttp/truststore_server Binary files differ
diff --git a/containers/jersey-servlet-core/pom.xml b/containers/jersey-servlet-core/pom.xml new file mode 100644 index 0000000..4bc5700 --- /dev/null +++ b/containers/jersey-servlet-core/pom.xml
@@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-servlet-core</artifactId> + <packaging>jar</packaging> + <name>jersey-container-servlet-core</name> + + <description>Jersey core Servlet 2.x implementation</description> + + <dependencies> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + <version>${servlet2.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>javax.persistence</groupId> + <artifactId>persistence-api</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- Note: When you're changing these properties change them also in bundles/jax-rs-ri/bundle/pom.xml. --> + <Import-Package> + javax.persistence.*;resolution:=optional, + javax.servlet.*;version="[2.4,5.0)", + javax.annotation.*;version=!, + * + </Import-Package> + <Export-Package>org.glassfish.jersey.servlet.*</Export-Package> + </instructions> + <unpackBundle>true</unpackBundle> + </configuration> + </plugin> + </plugins> + </build> + +</project>
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java new file mode 100644 index 0000000..f54fc00 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java
@@ -0,0 +1,681 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.util.ExtendedLogger; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.internal.ContainerUtils; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.server.spi.ContainerLifecycleListener; +import org.glassfish.jersey.servlet.internal.LocalizationMessages; +import org.glassfish.jersey.servlet.internal.ResponseWriter; +import org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider; +import org.glassfish.jersey.uri.UriComponent; + +/** + * A {@link javax.servlet.Servlet} or {@link Filter} for deploying root resource classes. + * <p /> + * The following sections make reference to initialization parameters. Unless + * otherwise specified the initialization parameters apply to both server + * and filter initialization parameters. + * <p /> + * The servlet or filter may be configured to have an initialization + * parameter {@value ServletProperties#JAXRS_APPLICATION_CLASS} + * (see {@link org.glassfish.jersey.servlet.ServletProperties#JAXRS_APPLICATION_CLASS}) and whose value is a + * fully qualified name of a class that implements {@link javax.ws.rs.core.Application}. + * The class is instantiated as a singleton component + * managed by the runtime, and injection may be performed (the artifacts that + * may be injected are limited to injectable providers registered when + * the servlet or filter is configured). + * <p /> + * If the initialization parameter {@value ServletProperties#JAXRS_APPLICATION_CLASS} + * is not present and a initialization parameter {@value org.glassfish.jersey.server.ServerProperties#PROVIDER_PACKAGES} + * is present (see {@link ServerProperties#PROVIDER_PACKAGES}) a new instance of + * {@link ResourceConfig} with this configuration is created. The initialization parameter + * {@value org.glassfish.jersey.server.ServerProperties#PROVIDER_PACKAGES} MUST be set to provide one or + * more package names. Each package name MUST be separated by ';'. + * <p /> + * If none of the above resource configuration related initialization parameters + * are present a new instance of {@link ResourceConfig} with {@link WebAppResourcesScanner} + * is created. The initialization parameter {@value org.glassfish.jersey.server.ServerProperties#PROVIDER_CLASSPATH} + * is present (see {@link ServerProperties#PROVIDER_CLASSPATH}) MAY be + * set to provide one or more resource paths. Each path MUST be separated by ';'. + * If the initialization parameter is not present then the following resource + * paths are utilized: {@code "/WEB-INF/lib"} and {@code "/WEB-INF/classes"}. + * <p /> + * All initialization parameters are added as properties of the created + * {@link ResourceConfig}. + * <p /> + * A new {@link org.glassfish.jersey.server.ApplicationHandler} instance will be created and configured such + * that the following classes may be injected onto a root resource, provider + * and {@link javax.ws.rs.core.Application} classes using {@link javax.ws.rs.core.Context + * @Context} annotation: + * {@link HttpServletRequest}, {@link HttpServletResponse}, + * {@link ServletContext}, {@link javax.servlet.ServletConfig} and {@link WebConfig}. + * If this class is used as a Servlet then the {@link javax.servlet.ServletConfig} class may + * be injected. If this class is used as a servlet filter then the {@link FilterConfig} + * class may be injected. {@link WebConfig} may be injected to abstract + * servlet or filter deployment. + * <p /> + * Persistence units that may be injected must be configured in web.xml + * in the normal way plus an additional servlet parameter to enable the + * Jersey servlet to locate them in JNDI. E.g. with the following + * persistence unit configuration: + * <pre>{@code + * <persistence-unit-ref> + * <persistence-unit-ref-name>persistence/widget</persistence-unit-ref-name> + * <persistence-unit-name>WidgetPU</persistence-unit-name> + * </persistence-unit-ref> + * }</pre> + * the Jersey servlet requires an additional servlet parameter as + * follows: + * <pre>{@code + * <init-param> + * <param-name>unit:WidgetPU</param-name> + * <param-value>persistence/widget</param-value> + * </init-param> + * }</pre> + * Given the above, Jersey will inject the {@link javax.persistence.EntityManagerFactory EntityManagerFactory} found + * at {@code java:comp/env/persistence/widget} in JNDI when encountering a + * field or parameter annotated with {@code @PersistenceUnit(unitName="WidgetPU")}. + * + * @author Paul Sandoz + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Michal Gajdos + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +public class ServletContainer extends HttpServlet implements Filter, Container { + + private static final long serialVersionUID = 3932047066686065219L; + private static final ExtendedLogger LOGGER = + new ExtendedLogger(Logger.getLogger(ServletContainer.class.getName()), Level.FINEST); + + private transient FilterConfig filterConfig; + private transient WebComponent webComponent; + private transient ResourceConfig resourceConfig; + private transient Pattern staticContentPattern; + private transient String filterContextPath; + private transient List<String> filterUrlMappings; + + private transient volatile ContainerLifecycleListener containerListener; + + /** + * Initiate the Web component. + * + * @param webConfig the Web configuration. + * @throws javax.servlet.ServletException in case of an initialization failure + */ + protected void init(final WebConfig webConfig) throws ServletException { + webComponent = new WebComponent(webConfig, resourceConfig); + containerListener = webComponent.appHandler; + containerListener.onStartup(this); + } + + /** + * Create Jersey Servlet container. + */ + public ServletContainer() { + } + + /** + * Create Jersey Servlet container. + * + * @param resourceConfig container configuration. + */ + public ServletContainer(final ResourceConfig resourceConfig) { + this.resourceConfig = resourceConfig; + } + + /** + * Dispatches client requests to the protected + * {@code service} method. There's no need to + * override this method. + * + * @param req the {@link HttpServletRequest} object that + * contains the request the client made of + * the servlet + * @param res the {@link HttpServletResponse} object that + * contains the response the servlet returns + * to the client + * @throws IOException if an input or output error occurs + * while the servlet is handling the + * HTTP request + * @throws ServletException if the HTTP request cannot + * be handled + * @see javax.servlet.Servlet#service + */ + @Override + public void service(final ServletRequest req, final ServletResponse res) + throws ServletException, IOException { + final HttpServletRequest request; + final HttpServletResponse response; + + if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) { + throw new ServletException("non-HTTP request or response"); + } + + request = (HttpServletRequest) req; + response = (HttpServletResponse) res; + + service(request, response); + } + + /** + * Receives standard HTTP requests from the public {@code service} method and dispatches + * them to the {@code do}<i>XXX</i> methods defined in + * this class. This method is an HTTP-specific version of the + * {@link javax.servlet.Servlet#service} method. There's no + * need to override this method. + * + * @param request the {@link HttpServletRequest} object that + * contains the request the client made of + * the servlet + * @param response the {@link HttpServletResponse} object that + * contains the response the servlet returns + * to the client + * @throws IOException if an input or output error occurs + * while the servlet is handling the + * HTTP request + * @throws ServletException if the HTTP request + * cannot be handled + * @see javax.servlet.Servlet#service + */ + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + /** + * There is an annoying edge case where the service method is + * invoked for the case when the URI is equal to the deployment URL + * minus the '/', for example http://locahost:8080/HelloWorldWebApp + */ + final String servletPath = request.getServletPath(); + final StringBuffer requestUrl = request.getRequestURL(); + final String requestURI = request.getRequestURI(); + + // final String pathInfo = request.getPathInfo(); + // final boolean checkPathInfo = pathInfo == null || pathInfo.isEmpty() || pathInfo.equals("/"); + // if (checkPathInfo && !request.getRequestURI().endsWith("/")) { + // Only do this if the last segment of the servlet path does not contain '.' + // This handles the case when the extension mapping is used with the servlet + // see issue 506 + // This solution does not require parsing the deployment descriptor, + // however still leaves it broken for the very rare case if a standard path + // servlet mapping would include dot in the last segment (e.g. /.webresources/*) + // and somebody would want to hit the root resource without the trailing slash + // final int i = servletPath.lastIndexOf('/'); + // if (servletPath.substring(i + 1).indexOf('.') < 0) { + // TODO (+ handle request URL with invalid characters - see the creation of absoluteUriBuilder below) + // if (webComponent.getResourceConfig().getFeature(ResourceConfig.FEATURE_REDIRECT)) { + // URI l = UriBuilder.fromUri(request.getRequestURL().toString()). + // path("/"). + // replaceQuery(request.getQueryString()).build(); + // + // response.setStatus(307); + // response.setHeader("Location", l.toASCIIString()); + // return; + // } else { + // pathInfo = "/"; + // requestURL.append("/"); + // requestURI += "/"; + // } + // } + // } + + /** + * The HttpServletRequest.getRequestURL() contains the complete URI + * minus the query and fragment components. + */ + final UriBuilder absoluteUriBuilder; + try { + absoluteUriBuilder = UriBuilder.fromUri(requestUrl.toString()); + } catch (final IllegalArgumentException iae) { + setResponseForInvalidUri(response, iae); + return; + } + + /** + * The HttpServletRequest.getPathInfo() and + * HttpServletRequest.getServletPath() are in decoded form. + * + * On some servlet implementations the getPathInfo() removed + * contiguous '/' characters. This is problematic if URIs + * are embedded, for example as the last path segment. + * We need to work around this and not use getPathInfo + * for the decodedPath. + */ + final String decodedBasePath = request.getContextPath() + servletPath + "/"; + + final String encodedBasePath = UriComponent.encode(decodedBasePath, + UriComponent.Type.PATH); + + if (!decodedBasePath.equals(encodedBasePath)) { + throw new ProcessingException("The servlet context path and/or the " + + "servlet path contain characters that are percent encoded"); + } + + final URI baseUri; + final URI requestUri; + try { + baseUri = absoluteUriBuilder.replacePath(encodedBasePath).build(); + String queryParameters = ContainerUtils.encodeUnsafeCharacters(request.getQueryString()); + if (queryParameters == null) { + queryParameters = ""; + } + + requestUri = absoluteUriBuilder.replacePath(requestURI) + .replaceQuery(queryParameters) + .build(); + } catch (final UriBuilderException | IllegalArgumentException ex) { + setResponseForInvalidUri(response, ex); + return; + } + + service(baseUri, requestUri, request, response); + } + + private void setResponseForInvalidUri(final HttpServletResponse response, final Throwable throwable) throws IOException { + LOGGER.log(Level.FINER, "Error while processing request.", throwable); + + final Response.Status badRequest = Response.Status.BAD_REQUEST; + if (webComponent.configSetStatusOverSendError) { + response.reset(); + //noinspection deprecation + response.setStatus(badRequest.getStatusCode(), badRequest.getReasonPhrase()); + } else { + response.sendError(badRequest.getStatusCode(), badRequest.getReasonPhrase()); + } + } + + @Override + public void destroy() { + super.destroy(); + + final ContainerLifecycleListener listener = containerListener; + if (listener != null) { + listener.onShutdown(this); + } + } + + @Override + public void init() throws ServletException { + init(new WebServletConfig(this)); + } + + /** + * Dispatch client requests to a resource class. + * + * @param baseUri the base URI of the request. + * @param requestUri the URI of the request. + * @param request the {@link javax.servlet.http.HttpServletRequest} object that contains the request the client made to + * the Web component. + * @param response the {@link javax.servlet.http.HttpServletResponse} object that contains the response the Web component + * returns to the client. + * @return lazily initialized response status code {@link Value value provider}. If not resolved in the moment of call to + * {@link Value#get()}, {@code -1} is returned. + * @throws IOException if an input or output error occurs while the Web component is handling the HTTP request. + * @throws ServletException if the HTTP request cannot be handled. + */ + public Value<Integer> service(final URI baseUri, final URI requestUri, final HttpServletRequest request, + final HttpServletResponse response) throws ServletException, IOException { + return webComponent.service(baseUri, requestUri, request, response); + } + + /** + * Dispatch client requests to a resource class and returns {@link ResponseWriter}, + * Servlet's {@link org.glassfish.jersey.server.spi.ContainerResponseWriter} implementation. + * + * @param baseUri the base URI of the request. + * @param requestUri the URI of the request. + * @param request the {@link javax.servlet.http.HttpServletRequest} object that contains the request the client made to + * the Web component. + * @param response the {@link javax.servlet.http.HttpServletResponse} object that contains the response the Web component + * returns to the client. + * @return returns {@link ResponseWriter}, Servlet's {@link org.glassfish.jersey.server.spi.ContainerResponseWriter} + * implementation, into which processed request response was written to. + * @throws IOException if an input or output error occurs while the Web component is handling the HTTP request. + * @throws ServletException if the HTTP request cannot be handled. + */ + private ResponseWriter serviceImpl(final URI baseUri, final URI requestUri, final HttpServletRequest request, + final HttpServletResponse response) throws ServletException, IOException { + return webComponent.serviceImpl(baseUri, requestUri, request, response); + } + + // Filter + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + this.filterConfig = filterConfig; + init(new WebFilterConfig(filterConfig)); + + final String regex = (String) getConfiguration().getProperty(ServletProperties.FILTER_STATIC_CONTENT_REGEX); + if (regex != null && !regex.isEmpty()) { + try { + staticContentPattern = Pattern.compile(regex); + } catch (final PatternSyntaxException ex) { + throw new ContainerException(LocalizationMessages.INIT_PARAM_REGEX_SYNTAX_INVALID( + regex, ServletProperties.FILTER_STATIC_CONTENT_REGEX), ex); + } + } + + this.filterContextPath = filterConfig.getInitParameter(ServletProperties.FILTER_CONTEXT_PATH); + if (filterContextPath != null) { + if (filterContextPath.isEmpty()) { + filterContextPath = null; + } else { + if (!filterContextPath.startsWith("/")) { + filterContextPath = '/' + filterContextPath; + } + if (filterContextPath.endsWith("/")) { + filterContextPath = filterContextPath.substring(0, filterContextPath.length() - 1); + } + } + } + + // get the url-pattern defined (e.g.) in the filter-mapping section of web.xml + final FilterUrlMappingsProvider filterUrlMappingsProvider = getFilterUrlMappingsProvider(); + if (filterUrlMappingsProvider != null) { + filterUrlMappings = filterUrlMappingsProvider.getFilterUrlMappings(filterConfig); + } + + // we need either the url-pattern from the filter mapping (in case of Servlet 3) or specific init-param to + // determine the baseUri and request relative URI. If we do not have either one, the app will most likely + // not work (won't be accessible) + if (filterUrlMappings == null && filterContextPath == null) { + LOGGER.warning(LocalizationMessages.FILTER_CONTEXT_PATH_MISSING()); + } + } + + @Override + public void doFilter(final ServletRequest servletRequest, + final ServletResponse servletResponse, + final FilterChain filterChain) + throws IOException, ServletException { + try { + doFilter((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, filterChain); + } catch (final ClassCastException e) { + throw new ServletException("non-HTTP request or response", e); + } + } + + /** + * Get the servlet context for the servlet or filter, depending on + * how this class is registered. + * + * @return the servlet context for the servlet or filter. + */ + @Override + public ServletContext getServletContext() { + if (filterConfig != null) { + return filterConfig.getServletContext(); + } + + return super.getServletContext(); + } + + /** + * Dispatches client requests to the + * {@link #service(URI, URI, HttpServletRequest, HttpServletResponse)} method. + * <p /> + * If the servlet path matches the regular expression declared by the + * property {@link ServletProperties#FILTER_STATIC_CONTENT_REGEX} then the + * request is forwarded to the next filter in the filter chain so that the + * underlying servlet engine can process the request otherwise Jersey + * will process the request. + * + * @param request the {@link HttpServletRequest} object that + * contains the request the client made to + * the servlet. + * @param response the {@link HttpServletResponse} object that + * contains the response the servlet returns + * to the client. + * @param chain the chain of filters from which the next filter can be invoked. + * @throws java.io.IOException in case of an I/O error. + * @throws javax.servlet.ServletException in case of an error while executing the + * filter chain. + */ + public void doFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) + throws IOException, ServletException { + if (request.getAttribute("javax.servlet.include.request_uri") != null) { + final String includeRequestURI = (String) request.getAttribute("javax.servlet.include.request_uri"); + + if (!includeRequestURI.equals(request.getRequestURI())) { + doFilter(request, response, chain, + includeRequestURI, + (String) request.getAttribute("javax.servlet.include.servlet_path"), + (String) request.getAttribute("javax.servlet.include.query_string")); + return; + } + } + + /** + * JERSEY-880 - WAS interprets HttpServletRequest#getServletPath() and HttpServletRequest#getPathInfo() + * differently when accessing a static resource. + */ + final String servletPath = request.getServletPath() + + (request.getPathInfo() == null ? "" : request.getPathInfo()); + + doFilter(request, response, chain, + request.getRequestURI(), + servletPath, + request.getQueryString()); + } + + private void doFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, + final String requestURI, final String servletPath, final String queryString) + throws IOException, ServletException { + // if we match the static content regular expression lets delegate to + // the filter chain to use the default container servlets & handlers + final Pattern p = getStaticContentPattern(); + if (p != null && p.matcher(servletPath).matches()) { + chain.doFilter(request, response); + return; + } + + if (filterContextPath != null) { + if (!servletPath.startsWith(filterContextPath)) { + throw new ContainerException(LocalizationMessages.SERVLET_PATH_MISMATCH(servletPath, filterContextPath)); + //TODO: + // } else if (servletPath.length() == filterContextPath.length()) { + // // Path does not end in a slash, may need to redirect + // if (webComponent.getResourceConfig().getFeature(ResourceConfig.FEATURE_REDIRECT)) { + // URI l = UriBuilder.fromUri(request.getRequestURL().toString()). + // path("/"). + // replaceQuery(queryString).build(); + // + // response.setStatus(307); + // response.setHeader("Location", l.toASCIIString()); + // return; + // } else { + // requestURI += "/"; + // } + } + } + + final URI baseUri; + final URI requestUri; + try { + final UriBuilder absoluteUriBuilder = UriBuilder.fromUri(request.getRequestURL().toString()); + + // depending on circumstances, use the correct path to replace in the absolute request URI + final String pickedUrlMapping = pickUrlMapping(request.getRequestURL().toString(), filterUrlMappings); + final String replacingPath = pickedUrlMapping != null + ? pickedUrlMapping + : (filterContextPath != null ? filterContextPath : ""); + + baseUri = absoluteUriBuilder.replacePath(request.getContextPath()).path(replacingPath).path("/").build(); + + + requestUri = absoluteUriBuilder.replacePath(requestURI) + .replaceQuery(ContainerUtils.encodeUnsafeCharacters(queryString)) + .build(); + } catch (final IllegalArgumentException iae) { + setResponseForInvalidUri(response, iae); + return; + } + + final ResponseWriter responseWriter = serviceImpl(baseUri, requestUri, request, response); + + // If forwarding is configured and response is a 404 with no entity + // body then call the next filter in the chain + + if (webComponent.forwardOn404 && !response.isCommitted()) { + boolean hasEntity = false; + Response.StatusType status = null; + if (responseWriter.responseContextResolved()) { + final ContainerResponse responseContext = responseWriter.getResponseContext(); + hasEntity = responseContext.hasEntity(); + status = responseContext.getStatusInfo(); + } + if (!hasEntity && status == Response.Status.NOT_FOUND) { + // lets clear the response to OK before we forward to the next in the chain + // as OK is the default set by servlet containers before filters/servlets do any work + // so lets hide our footsteps and pretend we were never in the chain at all and let the + // next filter or servlet return the 404 if they can't find anything to return + // + // We could add an optional flag to disable this step if anyone can ever find a case where + // this causes a problem, though I suspect any problems will really be with downstream + // servlets not correctly setting an error status if they cannot find something to return + response.setStatus(HttpServletResponse.SC_OK); + chain.doFilter(request, response); + } + } + } + + /** + * Picks the most suitable url mapping (in case more than one is defined) based on the request URI. + * + * @param requestUri String representation of the request URI + * @param filterUrlMappings set of configured filter url-patterns + * @return the most suitable context path, or {@code null} if empty + */ + private String pickUrlMapping(final String requestUri, final List<String> filterUrlMappings) { + if (filterUrlMappings == null || filterUrlMappings.isEmpty()) { + return null; + } + + if (filterUrlMappings.size() == 1) { + return filterUrlMappings.get(0); + } + + for (final String pattern : filterUrlMappings) { + if (requestUri.contains(pattern)) { + return pattern; + } + } + + return null; + } + + /** + * Resolve the {@link FilterUrlMappingsProvider} service via hk2. + * + * Will only work in Servlet 3 container, as the older API version + * does not provide access to the filter mapping structure. + * + * @return {@code FilterContextPath} instance, if available, {@code null} otherwise. + */ + private FilterUrlMappingsProvider getFilterUrlMappingsProvider() { + FilterUrlMappingsProvider filterUrlMappingsProvider = null; + final Iterator<FilterUrlMappingsProvider> providers = Providers.getAllProviders( + getApplicationHandler().getInjectionManager(), FilterUrlMappingsProvider.class).iterator(); + if (providers.hasNext()) { + filterUrlMappingsProvider = providers.next(); + } + return filterUrlMappingsProvider; + } + + /** + * Get the static content path pattern. + * + * @return the {@link Pattern} compiled from a regular expression that is + * the property value of {@link ServletProperties#FILTER_STATIC_CONTENT_REGEX}. + * A {@code null} value will be returned if the property is not set or is + * an empty String. + */ + protected Pattern getStaticContentPattern() { + return staticContentPattern; + } + + @Override + public ResourceConfig getConfiguration() { + return webComponent.appHandler.getConfiguration(); + } + + @Override + public void reload() { + reload(getConfiguration()); + } + + @Override + public void reload(final ResourceConfig configuration) { + try { + containerListener.onShutdown(this); + + webComponent = new WebComponent(webComponent.webConfig, configuration); + containerListener = webComponent.appHandler; + containerListener.onReload(this); + containerListener.onStartup(this); + } catch (final ServletException ex) { + LOGGER.log(Level.SEVERE, "Reload failed", ex); + } + } + + @Override + public ApplicationHandler getApplicationHandler() { + return webComponent.appHandler; + } + + /** + * Get {@link WebComponent} used by this servlet container. + * + * @return The web component. + */ + @SuppressWarnings("UnusedDeclaration") + public WebComponent getWebComponent() { + return webComponent; + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletProperties.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletProperties.java new file mode 100644 index 0000000..e6ea54b --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletProperties.java
@@ -0,0 +1,157 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import org.glassfish.jersey.internal.util.PropertiesClass; + +/** + * Jersey servlet container configuration properties. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@PropertiesClass +public final class ServletProperties { + + /** + * If set, indicates the URL pattern of the Jersey servlet filter context path. + * <p> + * If the URL pattern of a filter is set to a base path and a wildcard, + * such as "/base/*", then this property can be used to declare a filter + * context path that behaves in the same manner as the Servlet context + * path for determining the base URI of the application. (Note that with + * the Servlet 2.x API it is not possible to determine the URL pattern + * without parsing the {@code web.xml}, hence why this property is necessary.) + * <p> + * The property is only applicable when {@link ServletContainer Jersey servlet + * container} is configured to run as a {@link javax.servlet.Filter}, otherwise this property + * will be ignored. + * <p> + * The value of the property may consist of one or more path segments separate by + * {@code '/'}. + * <p></p> + * A default value is not set. + * <p></p> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String FILTER_CONTEXT_PATH = "jersey.config.servlet.filter.contextPath"; + + /** + * If set to {@code true} and a 404 response with no entity body is returned + * from either the runtime or the application then the runtime forwards the + * request to the next filter in the filter chain. This enables another filter + * or the underlying servlet engine to process the request. Before the request + * is forwarded the response status is set to 200. + * <p> + * This property is an alternative to setting a {@link #FILTER_STATIC_CONTENT_REGEX + * static content regular expression} and requires less configuration. However, + * application code, such as methods corresponding to sub-resource locators, + * may be invoked when this feature is enabled. + * <p></p> + * The property is only applicable when {@link ServletContainer Jersey servlet + * container} is configured to run as a {@link javax.servlet.Filter}, otherwise + * this property will be ignored. + * <p></p> + * Application code, such as methods corresponding to sub-resource locators + * may be invoked when this feature is enabled. + * <p> + * The default value is {@code false}. + * <p></p> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String FILTER_FORWARD_ON_404 = "jersey.config.servlet.filter.forwardOn404"; + + /** + * If set the regular expression is used to match an incoming servlet path URI + * to some web page content such as static resources or JSPs to be handled + * by the underlying servlet engine. + * <p></p> + * The property is only applicable when {@link ServletContainer Jersey servlet + * container} is configured to run as a {@link javax.servlet.Filter}, otherwise + * this property will be ignored. If a servlet path matches this regular + * expression then the filter forwards the request to the next filter in the + * filter chain so that the underlying servlet engine can process the request + * otherwise Jersey will process the request. For example if you set the value + * to {@code /(image|css)/.*} then you can serve up images and CSS files + * for your Implicit or Explicit Views while still processing your JAX-RS + * resources. + * <p></p> + * The type of this property must be a String and the value must be a valid + * regular expression. + * <p></p> + * A default value is not set. + * <p></p> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String FILTER_STATIC_CONTENT_REGEX = "jersey.config.servlet.filter.staticContentRegex"; + + /** + * Application configuration initialization property whose value is a fully + * qualified class name of a class that implements {@link javax.ws.rs.core.Application}. + * <p></p> + * A default value is not set. + * <p></p> + * The name of the configuration property is <tt>{@value}</tt>. + */ + // TODO implement generic support + public static final String JAXRS_APPLICATION_CLASS = "javax.ws.rs.Application"; + + /** + * Indicates that Jersey should scan the whole web app for application-specific resources and + * providers. If the property is present and the value is not {@code false}, the whole web app + * will be scanned for JAX-RS root resources (annotated with {@link javax.ws.rs.Path @Path}) + * and providers (annotated with {@link javax.ws.rs.ext.Provider @Provider}). + * <p></p> + * The property value MUST be an instance of {@link String}. The allowed values are {@code true} + * and {@code false}. + * <p></p> + * A default value is not set. + * <p></p> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String PROVIDER_WEB_APP = "jersey.config.servlet.provider.webapp"; + + /** + * If {@code true} then query parameters will not be treated as form parameters (e.g. injectable using + * {@link javax.ws.rs.FormParam}) in case a Form request is processed by server. + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @since 2.16 + */ + public static final String QUERY_PARAMS_AS_FORM_PARAMS_DISABLED = "jersey.config.servlet.form.queryParams.disabled"; + + /** + * Identifies the object that will be used as a parent {@code HK2 ServiceLocator} in the Jersey + * {@link WebComponent}. + * <p></p> + * This property gives a possibility to use HK2 services that are registered and/or created + * outside of the Jersey server context. + * <p></p> + * By default this property is not set. + * <p></p> + * The name of the configuration property is <tt>{@value}</tt>. + */ + public static final String SERVICE_LOCATOR = "jersey.config.servlet.context.serviceLocator"; + + private ServletProperties() { + // prevents instantiation + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletPropertiesDelegate.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletPropertiesDelegate.java new file mode 100644 index 0000000..7a17521 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletPropertiesDelegate.java
@@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.util.Collection; +import java.util.Collections; + +import javax.servlet.http.HttpServletRequest; + +import org.glassfish.jersey.internal.PropertiesDelegate; + +/** + * @author Martin Matula + */ +class ServletPropertiesDelegate implements PropertiesDelegate { + private final HttpServletRequest request; + + public ServletPropertiesDelegate(HttpServletRequest request) { + this.request = request; + } + + @Override + public Object getProperty(String name) { + return request.getAttribute(name); + } + + @Override + public Collection<String> getPropertyNames() { + //noinspection unchecked + return Collections.list(request.getAttributeNames()); + } + + @Override + public void setProperty(String name, Object object) { + request.setAttribute(name, object); + } + + @Override + public void removeProperty(String name) { + request.removeAttribute(name); + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebAppResourcesScanner.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebAppResourcesScanner.java new file mode 100644 index 0000000..f80464a --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebAppResourcesScanner.java
@@ -0,0 +1,152 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Deque; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.servlet.ServletContext; + +import org.glassfish.jersey.server.internal.AbstractResourceFinderAdapter; +import org.glassfish.jersey.server.internal.scanning.JarFileScanner; +import org.glassfish.jersey.server.internal.scanning.ResourceFinderException; +import org.glassfish.jersey.server.internal.scanning.CompositeResourceFinder; + +/** + * A scanner that recursively scans resources within a Web application. + * + * @author Paul Sandoz + */ +final class WebAppResourcesScanner extends AbstractResourceFinderAdapter { + + private static final String[] paths = new String[] {"/WEB-INF/lib/", "/WEB-INF/classes/"}; + + private final ServletContext sc; + private CompositeResourceFinder compositeResourceFinder = new CompositeResourceFinder(); + + /** + * Scan from a set of web resource paths. + * <p/> + * + * @param sc {@link ServletContext}. + */ + WebAppResourcesScanner(final ServletContext sc) { + this.sc = sc; + + processPaths(paths); + } + + private void processPaths(final String... paths) { + for (final String path : paths) { + + final Set<String> resourcePaths = sc.getResourcePaths(path); + if (resourcePaths == null) { + break; + } + + compositeResourceFinder.push(new AbstractResourceFinderAdapter() { + + private final Deque<String> resourcePathsStack = new LinkedList<String>() { + + private static final long serialVersionUID = 3109256773218160485L; + + { + for (final String resourcePath : resourcePaths) { + push(resourcePath); + } + } + }; + + private String current; + private String next; + + @Override + public boolean hasNext() { + while (next == null && !resourcePathsStack.isEmpty()) { + next = resourcePathsStack.pop(); + + if (next.endsWith("/")) { + processPaths(next); + next = null; + } else if (next.endsWith(".jar")) { + try { + compositeResourceFinder.push(new JarFileScanner(sc.getResourceAsStream(next), "", true)); + } catch (final IOException ioe) { + throw new ResourceFinderException(ioe); + } + next = null; + } + } + + return next != null; + } + + @Override + public String next() { + if (next != null || hasNext()) { + current = next; + next = null; + return current; + } + + throw new NoSuchElementException(); + } + + @Override + public InputStream open() { + return sc.getResourceAsStream(current); + } + + @Override + public void reset() { + throw new UnsupportedOperationException(); + } + }); + + } + } + + @Override + public boolean hasNext() { + return compositeResourceFinder.hasNext(); + } + + @Override + public String next() { + return compositeResourceFinder.next(); + } + + @Override + public InputStream open() { + return compositeResourceFinder.open(); + } + + @Override + public void close() { + compositeResourceFinder.close(); + } + + @Override + public void reset() { + compositeResourceFinder = new CompositeResourceFinder(); + processPaths(paths); + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java new file mode 100644 index 0000000..84962be --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
@@ -0,0 +1,648 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.security.AccessController; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.ws.rs.RuntimeType; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.internal.ServiceFinderBinder; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.message.internal.HeaderValueException; +import org.glassfish.jersey.message.internal.MediaTypes; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.BackgroundSchedulerLiteral; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.internal.InternalServerProperties; +import org.glassfish.jersey.server.spi.RequestScopedInitializer; +import org.glassfish.jersey.servlet.internal.LocalizationMessages; +import org.glassfish.jersey.servlet.internal.PersistenceUnitBinder; +import org.glassfish.jersey.servlet.internal.ResponseWriter; +import org.glassfish.jersey.servlet.internal.ServletContainerProviderFactory; +import org.glassfish.jersey.servlet.internal.Utils; +import org.glassfish.jersey.servlet.internal.spi.ExtendedServletContainerProvider; +import org.glassfish.jersey.servlet.internal.spi.RequestContextProvider; +import org.glassfish.jersey.servlet.internal.spi.RequestScopedInitializerProvider; +import org.glassfish.jersey.servlet.internal.spi.ServletContainerProvider; +import org.glassfish.jersey.servlet.spi.AsyncContextDelegate; +import org.glassfish.jersey.servlet.spi.AsyncContextDelegateProvider; +import org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider; +import org.glassfish.jersey.uri.UriComponent; + +/** + * An common Jersey web component that may be extended by a Servlet and/or + * Filter implementation, or encapsulated by a Servlet or Filter implementation. + * + * @author Paul Sandoz + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Martin Matula + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +public class WebComponent { + + private static final Logger LOGGER = Logger.getLogger(WebComponent.class.getName()); + + private static final Type REQUEST_TYPE = (new GenericType<Ref<HttpServletRequest>>() {}).getType(); + private static final Type RESPONSE_TYPE = (new GenericType<Ref<HttpServletResponse>>() {}).getType(); + + private static final AsyncContextDelegate DEFAULT_ASYNC_DELEGATE = new AsyncContextDelegate() { + + @Override + public void suspend() throws IllegalStateException { + throw new UnsupportedOperationException(LocalizationMessages.ASYNC_PROCESSING_NOT_SUPPORTED()); + } + + @Override + public void complete() { + } + }; + + private final RequestScopedInitializerProvider requestScopedInitializer; + private final boolean requestResponseBindingExternalized; + + private static final RequestScopedInitializerProvider DEFAULT_REQUEST_SCOPE_INITIALIZER_PROVIDER = + context -> (RequestScopedInitializer) injectionManager -> { + injectionManager.<Ref<HttpServletRequest>>getInstance(REQUEST_TYPE).set(context.getHttpServletRequest()); + injectionManager.<Ref<HttpServletResponse>>getInstance(RESPONSE_TYPE).set(context.getHttpServletResponse()); + }; + + /** + * Return the first found {@link AsyncContextDelegateProvider} + * (via {@link Providers#getAllProviders(InjectionManager, Class)}) or {@code #DEFAULT_ASYNC_DELEGATE} if + * other delegate cannot be found. + * + * @return a non-null AsyncContextDelegateProvider. + */ + private AsyncContextDelegateProvider getAsyncExtensionDelegate() { + final Iterator<AsyncContextDelegateProvider> providers = Providers.getAllProviders(appHandler.getInjectionManager(), + AsyncContextDelegateProvider.class).iterator(); + if (providers.hasNext()) { + return providers.next(); + } + + return (request, response) -> DEFAULT_ASYNC_DELEGATE; + } + + @SuppressWarnings("JavaDoc") + private static class HttpServletRequestReferencingFactory extends ReferencingFactory<HttpServletRequest> { + + @Inject + public HttpServletRequestReferencingFactory(final Provider<Ref<HttpServletRequest>> referenceFactory) { + super(referenceFactory); + } + } + + @SuppressWarnings("JavaDoc") + private static class HttpServletResponseReferencingFactory extends ReferencingFactory<HttpServletResponse> { + + @Inject + public HttpServletResponseReferencingFactory(final Provider<Ref<HttpServletResponse>> referenceFactory) { + super(referenceFactory); + } + } + + private final class WebComponentBinder extends AbstractBinder { + + private final Map<String, Object> applicationProperties; + + /** + * Create binder for {@link WebComponent} passing a map of properties to determine whether certain features are allowed + * or + * not. + * + * @param applicationProperties map of properties to determine whether certain features are allowed or not. + */ + private WebComponentBinder(final Map<String, Object> applicationProperties) { + this.applicationProperties = applicationProperties; + } + + @Override + protected void configure() { + + if (!requestResponseBindingExternalized) { + + // request + bindFactory(HttpServletRequestReferencingFactory.class).to(HttpServletRequest.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + + bindFactory(ReferencingFactory.referenceFactory()) + .to(new GenericType<Ref<HttpServletRequest>>() {}).in(RequestScoped.class); + + // response + bindFactory(HttpServletResponseReferencingFactory.class).to(HttpServletResponse.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.referenceFactory()) + .to(new GenericType<Ref<HttpServletResponse>>() {}).in(RequestScoped.class); + } + + bindFactory(webConfig::getServletContext).to(ServletContext.class).in(Singleton.class); + + final ServletConfig servletConfig = webConfig.getServletConfig(); + if (webConfig.getConfigType() == WebConfig.ConfigType.ServletConfig) { + bindFactory(() -> servletConfig).to(ServletConfig.class).in(Singleton.class); + + // @PersistenceUnit + final Enumeration initParams = servletConfig.getInitParameterNames(); + while (initParams.hasMoreElements()) { + final String initParamName = (String) initParams.nextElement(); + + if (initParamName.startsWith(PersistenceUnitBinder.PERSISTENCE_UNIT_PREFIX)) { + install(new PersistenceUnitBinder(servletConfig)); + break; + } + } + } else { + bindFactory(webConfig::getFilterConfig).to(FilterConfig.class).in(Singleton.class); + } + + bindFactory(() -> webConfig).to(WebConfig.class).in(Singleton.class); + + install(new ServiceFinderBinder<>(AsyncContextDelegateProvider.class, applicationProperties, RuntimeType.SERVER)); + install(new ServiceFinderBinder<>(FilterUrlMappingsProvider.class, applicationProperties, RuntimeType.SERVER)); + } + } + + /** + * Jersey application handler. + */ + final ApplicationHandler appHandler; + + /** + * Jersey background task scheduler - used for scheduling request timeout event handling tasks. + */ + final ScheduledExecutorService backgroundTaskScheduler; + + /** + * Web component configuration. + */ + final WebConfig webConfig; + + /** + * If {@code true} and deployed as filter, the unmatched requests will be forwarded. + */ + final boolean forwardOn404; + + /** + * Cached value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR}. + * If {@code true} method {@link HttpServletResponse#setStatus} is used over {@link HttpServletResponse#sendError}. + */ + final boolean configSetStatusOverSendError; + + /** + * Asynchronous context delegate provider. + */ + private final AsyncContextDelegateProvider asyncExtensionDelegate; + + /** + * Flag whether query parameters should be kept as entity form params if a servlet filter consumes entity and + * Jersey has to retrieve form params from servlet request parameters. + */ + private final boolean queryParamsAsFormParams; + + /** + * Create and initialize new web component instance. + * + * @param webConfig we component configuration. + * @param resourceConfig Jersey application configuration. + * @throws ServletException in case the Jersey application cannot be created from the supplied + * resource configuration. + */ + public WebComponent(final WebConfig webConfig, ResourceConfig resourceConfig) throws ServletException { + + this.webConfig = webConfig; + + if (resourceConfig == null) { + resourceConfig = createResourceConfig(webConfig); + } + + + final ServletContainerProvider[] allServletContainerProviders = + ServletContainerProviderFactory.getAllServletContainerProviders(); + + // SPI/extension hook to configure ResourceConfig + configure(resourceConfig, allServletContainerProviders); + + boolean rrbExternalized = false; + RequestScopedInitializerProvider rsiProvider = null; + + for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) { + if (servletContainerProvider instanceof ExtendedServletContainerProvider) { + final ExtendedServletContainerProvider extendedProvider = + (ExtendedServletContainerProvider) servletContainerProvider; + + if (extendedProvider.bindsServletRequestResponse()) { + rrbExternalized = true; + } + if (rsiProvider == null) { // try to take the first non-null provider + rsiProvider = extendedProvider.getRequestScopedInitializerProvider(); + } + } + } + + requestScopedInitializer = rsiProvider != null ? rsiProvider : DEFAULT_REQUEST_SCOPE_INITIALIZER_PROVIDER; + requestResponseBindingExternalized = rrbExternalized; + + final AbstractBinder webComponentBinder = new WebComponentBinder(resourceConfig.getProperties()); + resourceConfig.register(webComponentBinder); + + final Object locator = webConfig.getServletContext() + .getAttribute(ServletProperties.SERVICE_LOCATOR); + + this.appHandler = new ApplicationHandler(resourceConfig, webComponentBinder, locator); + + this.asyncExtensionDelegate = getAsyncExtensionDelegate(); + this.forwardOn404 = webConfig.getConfigType() == WebConfig.ConfigType.FilterConfig + && resourceConfig.isProperty(ServletProperties.FILTER_FORWARD_ON_404); + this.queryParamsAsFormParams = !resourceConfig.isProperty(ServletProperties.QUERY_PARAMS_AS_FORM_PARAMS_DISABLED); + this.configSetStatusOverSendError = ServerProperties.getValue(resourceConfig.getProperties(), + ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, false, Boolean.class); + this.backgroundTaskScheduler = appHandler.getInjectionManager() + .getInstance(ScheduledExecutorService.class, BackgroundSchedulerLiteral.INSTANCE); + } + + /** + * Dispatch client requests to a resource class. + * + * @param baseUri the base URI of the request. + * @param requestUri the URI of the request. + * @param servletRequest the {@link javax.servlet.http.HttpServletRequest} object that + * contains the request the client made to + * the Web component. + * @param servletResponse the {@link javax.servlet.http.HttpServletResponse} object that + * contains the response the Web component returns + * to the client. + * @return lazily initialized response status code {@link Value value provider}. If not resolved in the moment of call to + * {@link Value#get()}, {@code -1} is returned. + * @throws java.io.IOException if an input or output error occurs + * while the Web component is handling the + * HTTP request. + * @throws javax.servlet.ServletException if the HTTP request cannot be handled. + */ + public Value<Integer> service( + final URI baseUri, + final URI requestUri, + final HttpServletRequest servletRequest, + final HttpServletResponse servletResponse) throws ServletException, IOException { + final ResponseWriter responseWriter = serviceImpl(baseUri, requestUri, servletRequest, servletResponse); + return Values.lazy(new Value<Integer>() { + @Override + public Integer get() { + return responseWriter.responseContextResolved() ? responseWriter.getResponseStatus() : -1; + } + }); + } + + /** + * Dispatch client requests to a resource class. + * + * @param baseUri the base URI of the request. + * @param requestUri the URI of the request. + * @param servletRequest the {@link javax.servlet.http.HttpServletRequest} object that + * contains the request the client made to + * the Web component. + * @param servletResponse the {@link javax.servlet.http.HttpServletResponse} object that + * contains the response the Web component returns + * to the client. + * @return returns {@link ResponseWriter}, Servlet's {@link org.glassfish.jersey.server.spi.ContainerResponseWriter} + * implementation, into which processed request response was written to. + * @throws java.io.IOException if an input or output error occurs + * while the Web component is handling the + * HTTP request. + * @throws javax.servlet.ServletException if the HTTP request cannot be handled. + */ + /* package */ ResponseWriter serviceImpl( + final URI baseUri, + final URI requestUri, + final HttpServletRequest servletRequest, + final HttpServletResponse servletResponse) throws ServletException, IOException { + + final ResponseWriter responseWriter = new ResponseWriter( + forwardOn404, + configSetStatusOverSendError, + servletResponse, + asyncExtensionDelegate.createDelegate(servletRequest, servletResponse), + backgroundTaskScheduler); + + try { + final ContainerRequest requestContext = new ContainerRequest(baseUri, requestUri, servletRequest.getMethod(), + getSecurityContext(servletRequest), new ServletPropertiesDelegate(servletRequest)); + + initContainerRequest(requestContext, servletRequest, servletResponse, responseWriter); + + appHandler.handle(requestContext); + } catch (final HeaderValueException hve) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, LocalizationMessages.HEADER_VALUE_READ_FAILED(), hve); + } + + final Response.Status status = Response.Status.BAD_REQUEST; + + if (configSetStatusOverSendError) { + servletResponse.reset(); + //noinspection deprecation + servletResponse.setStatus(status.getStatusCode(), status.getReasonPhrase()); + } else { + servletResponse.sendError(status.getStatusCode(), status.getReasonPhrase()); + } + } catch (final Exception e) { + throw new ServletException(e); + } + return responseWriter; + } + + /** + * Initialize {@code ContainerRequest} instance to used used to handle {@code servletRequest}. + */ + private void initContainerRequest( + final ContainerRequest requestContext, + final HttpServletRequest servletRequest, + final HttpServletResponse servletResponse, + final ResponseWriter responseWriter) throws IOException { + + requestContext.setEntityStream(servletRequest.getInputStream()); + requestContext.setRequestScopedInitializer(requestScopedInitializer.get(new RequestContextProvider() { + @Override + public HttpServletRequest getHttpServletRequest() { + return servletRequest; + } + @Override + public HttpServletResponse getHttpServletResponse() { + return servletResponse; + } + })); + requestContext.setWriter(responseWriter); + + addRequestHeaders(servletRequest, requestContext); + // Check if any servlet filters have consumed a request entity + // of the media type application/x-www-form-urlencoded + // This can happen if a filter calls request.getParameter(...) + filterFormParameters(servletRequest, requestContext); + } + + /** + * Get default {@link javax.ws.rs.core.SecurityContext} for given {@code request}. + * + * @param request http servlet request to create a security context for. + * @return a non-null security context instance. + */ + private static SecurityContext getSecurityContext(final HttpServletRequest request) { + return new SecurityContext() { + + @Override + public Principal getUserPrincipal() { + return request.getUserPrincipal(); + } + + @Override + public boolean isUserInRole(final String role) { + return request.isUserInRole(role); + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public String getAuthenticationScheme() { + return request.getAuthType(); + } + }; + } + + /** + * Create a {@link ResourceConfig} instance from given {@link WebConfig}. + * + * @param config web config to create resource config from. + * @return resource config instance. + * @throws ServletException if an error has occurred. + */ + private static ResourceConfig createResourceConfig(final WebConfig config) throws ServletException { + final ServletContext servletContext = config.getServletContext(); + + // check if ResourceConfig has already been created, if so use it + ResourceConfig resourceConfig = Utils.retrieve(config.getServletContext(), config.getName()); + if (resourceConfig != null) { + return resourceConfig; + } + + final Map<String, Object> initParams = getInitParams(config); + final Map<String, Object> contextParams = Utils.getContextParams(servletContext); + + // check if the JAX-RS application config class property is present + final String jaxrsApplicationClassName = config.getInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS); + + if (jaxrsApplicationClassName == null) { + // If no resource config class property is present, create default config + resourceConfig = new ResourceConfig().addProperties(initParams).addProperties(contextParams); + + final String webApp = config.getInitParameter(ServletProperties.PROVIDER_WEB_APP); + if (webApp != null && !"false".equals(webApp)) { + resourceConfig.registerFinder(new WebAppResourcesScanner(servletContext)); + } + return resourceConfig; + } + + try { + final Class<? extends javax.ws.rs.core.Application> jaxrsApplicationClass = AccessController.doPrivileged( + ReflectionHelper.<javax.ws.rs.core.Application>classForNameWithExceptionPEA(jaxrsApplicationClassName) + ); + + if (javax.ws.rs.core.Application.class.isAssignableFrom(jaxrsApplicationClass)) { + return ResourceConfig.forApplicationClass(jaxrsApplicationClass) + .addProperties(initParams).addProperties(contextParams); + } else { + throw new ServletException(LocalizationMessages.RESOURCE_CONFIG_PARENT_CLASS_INVALID( + jaxrsApplicationClassName, javax.ws.rs.core.Application.class)); + } + } catch (final PrivilegedActionException e) { + throw new ServletException( + LocalizationMessages.RESOURCE_CONFIG_UNABLE_TO_LOAD(jaxrsApplicationClassName), e.getCause()); + } catch (final ClassNotFoundException e) { + throw new ServletException(LocalizationMessages.RESOURCE_CONFIG_UNABLE_TO_LOAD(jaxrsApplicationClassName), e); + } + } + + /** + * SPI/extension hook to configure ResourceConfig. + * + * @param resourceConfig Jersey application configuration. + * @throws ServletException if an error has occurred. + */ + private void configure(final ResourceConfig resourceConfig, + final ServletContainerProvider[] allServletContainerProviders) throws ServletException { + + for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) { + servletContainerProvider.configure(resourceConfig); + } + } + + /** + * Copy request headers present in {@code request} into {@code requestContext} ignoring {@code null} values. + * + * @param request http servlet request to copy headers from. + * @param requestContext container request to copy headers to. + */ + @SuppressWarnings("unchecked") + private void addRequestHeaders(final HttpServletRequest request, final ContainerRequest requestContext) { + final Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + final String name = names.nextElement(); + + final Enumeration<String> values = request.getHeaders(name); + while (values.hasMoreElements()) { + final String value = values.nextElement(); + if (value != null) { // filter out null values + requestContext.header(name, value); + } + } + } + } + + /** + * Extract init params from {@link WebConfig}. + * + * @param webConfig actual servlet context. + * @return map representing current init parameters. + */ + private static Map<String, Object> getInitParams(final WebConfig webConfig) { + final Map<String, Object> props = new HashMap<>(); + final Enumeration names = webConfig.getInitParameterNames(); + while (names.hasMoreElements()) { + final String name = (String) names.nextElement(); + props.put(name, webConfig.getInitParameter(name)); + } + return props; + } + + /** + * Extract parameters contained in {@link HttpServletRequest servlet request} and put them into + * {@link ContainerRequest container request} under + * {@value org.glassfish.jersey.server.internal.InternalServerProperties#FORM_DECODED_PROPERTY} property (as {@link Form} + * instance). + * + * @param servletRequest http servlet request to extract params from. + * @param containerRequest container request to put {@link Form} property to. + */ + private void filterFormParameters(final HttpServletRequest servletRequest, final ContainerRequest containerRequest) { + if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE, containerRequest.getMediaType()) + && !containerRequest.hasEntity()) { + final Form form = new Form(); + final Enumeration parameterNames = servletRequest.getParameterNames(); + + final String queryString = servletRequest.getQueryString(); + final List<String> queryParams = queryString != null ? getDecodedQueryParamList(queryString) + : Collections.<String>emptyList(); + + final boolean keepQueryParams = queryParamsAsFormParams || queryParams.isEmpty(); + final MultivaluedMap<String, String> formMap = form.asMap(); + + while (parameterNames.hasMoreElements()) { + final String name = (String) parameterNames.nextElement(); + final List<String> values = Arrays.asList(servletRequest.getParameterValues(name)); + + formMap.put(name, keepQueryParams ? values : filterQueryParams(name, values, queryParams)); + } + + if (!formMap.isEmpty()) { + containerRequest.setProperty(InternalServerProperties.FORM_DECODED_PROPERTY, form); + + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.log(Level.WARNING, LocalizationMessages.FORM_PARAM_CONSUMED(containerRequest.getRequestUri())); + } + } + } + } + + private List<String> getDecodedQueryParamList(final String queryString) { + final List<String> params = new ArrayList<>(); + for (final String param : queryString.split("&")) { + params.add(UriComponent.decode(param, UriComponent.Type.QUERY_PARAM)); + } + return params; + } + + /** + * From given list of values remove values that represents values of query params of the same name as the processed form + * parameter. + * + * @param name name of form/query parameter. + * @param values values of form/query parameter. + * @param params collection of unprocessed query parameters. + * @return list of form param values for given name without values of query param of the same name. + */ + private List<String> filterQueryParams(final String name, final List<String> values, final Collection<String> params) { + return values.stream() + .filter(s -> !params.remove(name + "=" + s) && !params.remove(name + "[]=" + s)) + .collect(Collectors.toList()); + } + + /** + * Get {@link ApplicationHandler} used by this web component. + * + * @return The application handler + */ + public ApplicationHandler getAppHandler() { + return appHandler; + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebConfig.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebConfig.java new file mode 100644 index 0000000..bb469c6 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebConfig.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.util.Enumeration; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +/** + * The Web configuration for accessing initialization parameters of a Web + * component and the {@link ServletContext}. + * + * @author Paul Sandoz + */ +public interface WebConfig { + + /** + * The web configuration type. + */ + public static enum ConfigType { + /** + * A configuration type of servlet configuration. + */ + ServletConfig, + /** + * A configuration type of filter configuration. + */ + FilterConfig + } + + /** + * Get the configuration type of this config. + * + * @return the configuration type. + */ + ConfigType getConfigType(); + + /** + * Get the corresponding ServletConfig if this WebConfig represents a {@link ServletConfig} + * + * @return servlet config or null + */ + ServletConfig getServletConfig(); + + /** + * Get the corresponding FilterConfig if this WebConfig represents a {@link FilterConfig} + * + * @return filter config or null + */ + FilterConfig getFilterConfig(); + + /** + * Get the name of the Web component. + * + * @return the name of the Web component. + */ + String getName(); + + /** + * Get an initialization parameter. + * + * @param name the parameter name. + * @return the parameter value, or null if the parameter is not present. + */ + String getInitParameter(String name); + + /** + * Get the enumeration of initialization parameter names. + * + * @return the enumeration of initialization parameter names. + */ + Enumeration getInitParameterNames(); + + /** + * Get the {@link ServletContext}. + * + * @return the {@link ServletContext}. + */ + ServletContext getServletContext(); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebFilterConfig.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebFilterConfig.java new file mode 100644 index 0000000..d375b85 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebFilterConfig.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.util.Enumeration; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +/** + * A filter based web config. Delegates all invocations to the filter + * configuration from the servlet api. + * + * @author Paul Sandoz + * @author Guilherme Silveira + */ +public final class WebFilterConfig implements WebConfig { + + private final FilterConfig filterConfig; + + public WebFilterConfig(final FilterConfig filterConfig) { + this.filterConfig = filterConfig; + } + + @Override + public WebConfig.ConfigType getConfigType() { + return WebConfig.ConfigType.FilterConfig; + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public FilterConfig getFilterConfig() { + return filterConfig; + } + + @Override + public String getName() { + return filterConfig.getFilterName(); + } + + @Override + public String getInitParameter(final String name) { + return filterConfig.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() { + return filterConfig.getInitParameterNames(); + } + + @Override + public ServletContext getServletContext() { + return filterConfig.getServletContext(); + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebServletConfig.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebServletConfig.java new file mode 100644 index 0000000..5c2a7a7 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebServletConfig.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet; + +import java.util.Enumeration; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +/** + * A servlet based web config. Delegates all invocations to the servlet + * configuration from the servlet api. + * + * @author Paul Sandoz + * @author guilherme silveira + */ +public final class WebServletConfig implements WebConfig { + + private final ServletContainer servlet; + + public WebServletConfig(final ServletContainer servlet) { + this.servlet = servlet; + } + + @Override + public WebConfig.ConfigType getConfigType() { + return WebConfig.ConfigType.ServletConfig; + } + + @Override + public ServletConfig getServletConfig() { + return servlet.getServletConfig(); + } + + @Override + public FilterConfig getFilterConfig() { + return null; + } + + @Override + public String getName() { + return servlet.getServletName(); + } + + @Override + public String getInitParameter(final String name) { + return servlet.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() { + return servlet.getInitParameterNames(); + } + + @Override + public ServletContext getServletContext() { + return servlet.getServletContext(); + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/PersistenceUnitBinder.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/PersistenceUnitBinder.java new file mode 100644 index 0000000..f1795c6 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/PersistenceUnitBinder.java
@@ -0,0 +1,115 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import java.lang.reflect.Proxy; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.GenericType; + +import javax.inject.Singleton; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceUnit; +import javax.servlet.ServletConfig; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.Injectee; +import org.glassfish.jersey.internal.inject.InjectionResolver; +import org.glassfish.jersey.server.ContainerException; + +/** + * {@link PersistenceUnit Persistence unit} injection binder. + * + * @author Michal Gajdos + */ +public class PersistenceUnitBinder extends AbstractBinder { + + private final ServletConfig servletConfig; + + /** + * Prefix of the persistence unit init param. + */ + public static final String PERSISTENCE_UNIT_PREFIX = "unit:"; + + /** + * Creates a new binder for {@link PersistenceUnitInjectionResolver}. + * + * @param servletConfig servlet config to find persistence units. + */ + public PersistenceUnitBinder(ServletConfig servletConfig) { + this.servletConfig = servletConfig; + } + + @Singleton + private static class PersistenceUnitInjectionResolver implements InjectionResolver<PersistenceUnit> { + + private final Map<String, String> persistenceUnits = new HashMap<>(); + + private PersistenceUnitInjectionResolver(ServletConfig servletConfig) { + for (final Enumeration parameterNames = servletConfig.getInitParameterNames(); parameterNames.hasMoreElements(); ) { + final String key = (String) parameterNames.nextElement(); + + if (key.startsWith(PERSISTENCE_UNIT_PREFIX)) { + persistenceUnits.put(key.substring(PERSISTENCE_UNIT_PREFIX.length()), + "java:comp/env/" + servletConfig.getInitParameter(key)); + } + } + } + + @Override + public Object resolve(Injectee injectee) { + if (!injectee.getRequiredType().equals(EntityManagerFactory.class)) { + return null; + } + + final PersistenceUnit annotation = injectee.getParent().getAnnotation(PersistenceUnit.class); + final String unitName = annotation.unitName(); + + if (!persistenceUnits.containsKey(unitName)) { + throw new ContainerException(LocalizationMessages.PERSISTENCE_UNIT_NOT_CONFIGURED(unitName)); + } + + return Proxy.newProxyInstance( + this.getClass().getClassLoader(), + new Class[] {EntityManagerFactory.class}, + new ThreadLocalNamedInvoker<EntityManagerFactory>(persistenceUnits.get(unitName))); + } + + @Override + public boolean isConstructorParameterIndicator() { + return false; + } + + @Override + public boolean isMethodParameterIndicator() { + return false; + } + + @Override + public Class<PersistenceUnit> getAnnotation() { + return PersistenceUnit.class; + } + } + + @Override + protected void configure() { + bind(new PersistenceUnitInjectionResolver(servletConfig)) + .to(new GenericType<InjectionResolver<PersistenceUnit>>() {}); + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java new file mode 100644 index 0000000..b96ebb7 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
@@ -0,0 +1,314 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.internal.JerseyRequestTimeoutHandler; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; +import org.glassfish.jersey.servlet.spi.AsyncContextDelegate; + +/** + * An internal implementation of {@link ContainerResponseWriter} for Servlet containers. + * The writer depends on provided {@link AsyncContextDelegate} to support async functionality. + * + * @author Paul Sandoz + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Martin Matula + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +public class ResponseWriter implements ContainerResponseWriter { + + private static final Logger LOGGER = Logger.getLogger(ResponseWriter.class.getName()); + + private final HttpServletResponse response; + /** + * Cached value of configuration property + * {@link org.glassfish.jersey.servlet.ServletProperties#FILTER_FORWARD_ON_404}. + */ + private final boolean useSetStatusOn404; + /** + * Cached value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR}. + * If {@code true} method {@link HttpServletResponse#setStatus} is used over {@link HttpServletResponse#sendError}. + */ + private final boolean configSetStatusOverSendError; + private final CompletableFuture<ContainerResponse> responseContext; + private final AsyncContextDelegate asyncExt; + + private final JerseyRequestTimeoutHandler requestTimeoutHandler; + + /** + * Creates a new instance to write a single Jersey response. + * + * @param useSetStatusOn404 true if status should be written explicitly when 404 is returned + * @param configSetStatusOverSendError if {@code true} method {@link HttpServletResponse#setStatus} is used over + * {@link HttpServletResponse#sendError} + * @param response original HttpResponseRequest + * @param asyncExt delegate to use for async features implementation + * @param timeoutTaskExecutor Jersey runtime executor used for background execution of timeout + * handling tasks. + */ + public ResponseWriter(final boolean useSetStatusOn404, + final boolean configSetStatusOverSendError, + final HttpServletResponse response, + final AsyncContextDelegate asyncExt, + final ScheduledExecutorService timeoutTaskExecutor) { + this.useSetStatusOn404 = useSetStatusOn404; + this.configSetStatusOverSendError = configSetStatusOverSendError; + this.response = response; + this.asyncExt = asyncExt; + this.responseContext = new CompletableFuture<>(); + + this.requestTimeoutHandler = new JerseyRequestTimeoutHandler(this, timeoutTaskExecutor); + } + + @Override + public boolean suspend(final long timeOut, final TimeUnit timeUnit, final TimeoutHandler timeoutHandler) { + try { + // Suspend the servlet. + asyncExt.suspend(); + } catch (final IllegalStateException ex) { + LOGGER.log(Level.WARNING, LocalizationMessages.SERVLET_REQUEST_SUSPEND_FAILED(), ex); + return false; + } + // Suspend the internal request timeout handler. + return requestTimeoutHandler.suspend(timeOut, timeUnit, timeoutHandler); + } + + @Override + public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throws IllegalStateException { + requestTimeoutHandler.setSuspendTimeout(timeOut, timeUnit); + } + + @Override + public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse responseContext) + throws ContainerException { + this.responseContext.complete(responseContext); + + // first set the content length, so that if headers have an explicit value, it takes precedence over this one + if (responseContext.hasEntity() && contentLength != -1 && contentLength < Integer.MAX_VALUE) { + response.setContentLength((int) contentLength); + } + // Note that the writing of headers MUST be performed before + // the invocation of sendError as on some Servlet implementations + // modification of the response headers will have no effect + // after the invocation of sendError. + final MultivaluedMap<String, String> headers = getResponseContext().getStringHeaders(); + for (final Map.Entry<String, List<String>> e : headers.entrySet()) { + final Iterator<String> it = e.getValue().iterator(); + if (!it.hasNext()) { + continue; + } + final String header = e.getKey(); + if (response.containsHeader(header)) { + // replace any headers previously set with values from Jersey container response. + response.setHeader(header, it.next()); + } + + while (it.hasNext()) { + response.addHeader(header, it.next()); + } + } + + final String reasonPhrase = responseContext.getStatusInfo().getReasonPhrase(); + if (reasonPhrase != null) { + response.setStatus(responseContext.getStatus(), reasonPhrase); + } else { + response.setStatus(responseContext.getStatus()); + } + + if (!responseContext.hasEntity()) { + return null; + } else { + try { + final OutputStream outputStream = response.getOutputStream(); + + // delegating output stream prevents closing the underlying servlet output stream, + // so that any Servlet filters in the chain can still write to the response after us. + return new NonCloseableOutputStreamWrapper(outputStream); + } catch (final IOException e) { + throw new ContainerException(e); + } + } + } + + @Override + public void commit() { + try { + callSendError(); + } finally { + requestTimeoutHandler.close(); + asyncExt.complete(); + } + } + + /** + * According to configuration and response processing status it calls {@link HttpServletResponse#sendError(int, String)} over + * common {@link HttpServletResponse#setStatus(int)}. + */ + private void callSendError() { + // call HttpServletResponse.sendError in case: + // - property ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR == false (default) + // - response NOT yet committed + // - response entity NOT set + // - response status code is 4xx or 5xx + // plus in case of Jersey as a Filter (JaaF): + // - response status code is 404 (Not Found) + // - property ServletProperties#FILTER_FORWARD_ON_404 == false (default) + if (!configSetStatusOverSendError && !response.isCommitted()) { + final ContainerResponse responseContext = getResponseContext(); + final boolean hasEntity = responseContext.hasEntity(); + final Response.StatusType status = responseContext.getStatusInfo(); + if (!hasEntity && status != null && status.getStatusCode() >= 400 + && !(useSetStatusOn404 && status == Response.Status.NOT_FOUND)) { + final String reason = status.getReasonPhrase(); + try { + if (reason == null || reason.isEmpty()) { + response.sendError(status.getStatusCode()); + } else { + response.sendError(status.getStatusCode(), reason); + } + } catch (final IOException ex) { + throw new ContainerException( + LocalizationMessages.EXCEPTION_SENDING_ERROR_RESPONSE(status, reason != null ? reason : "--"), + ex); + } + } + } + } + + @Override + public void failure(final Throwable error) { + try { + if (!response.isCommitted()) { + try { + if (configSetStatusOverSendError) { + response.reset(); + //noinspection deprecation + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), "Request failed."); + } else { + response.sendError(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), "Request failed."); + } + } catch (final IllegalStateException ex) { + // a race condition externally committing the response can still occur... + LOGGER.log(Level.FINER, "Unable to reset failed response.", ex); + } catch (final IOException ex) { + throw new ContainerException(LocalizationMessages.EXCEPTION_SENDING_ERROR_RESPONSE( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), "Request failed."), ex); + } finally { + asyncExt.complete(); + } + } + } finally { + requestTimeoutHandler.close(); + rethrow(error); + } + } + + @Override + public boolean enableResponseBuffering() { + return true; + } + + /** + * Rethrow the original exception as required by JAX-RS, 3.3.4 + * + * @param error throwable to be re-thrown + */ + private void rethrow(final Throwable error) { + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new ContainerException(error); + } + } + + /** + * Provides response status captured when + * {@link #writeResponseStatusAndHeaders(long, org.glassfish.jersey.server.ContainerResponse)} has been invoked. + * The method will block if the write method has not been called yet. + * + * @return response status + */ + public int getResponseStatus() { + return getResponseContext().getStatus(); + } + + public boolean responseContextResolved() { + return responseContext.isDone(); + } + + public ContainerResponse getResponseContext() { + try { + return responseContext.get(); + } catch (InterruptedException | ExecutionException ex) { + throw new ContainerException(ex); + } + } + + private static class NonCloseableOutputStreamWrapper extends OutputStream { + + private final OutputStream delegate; + + public NonCloseableOutputStreamWrapper(final OutputStream delegate) { + this.delegate = delegate; + } + + @Override + public void write(final int b) throws IOException { + delegate.write(b); + } + + @Override + public void write(final byte[] b) throws IOException { + delegate.write(b); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + delegate.write(b, off, len); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + // do not close - let the servlet container close the stream + } + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ServletContainerProviderFactory.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ServletContainerProviderFactory.java new file mode 100644 index 0000000..8dbeb80 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ServletContainerProviderFactory.java
@@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import org.glassfish.jersey.internal.ServiceFinder; +import org.glassfish.jersey.servlet.internal.spi.ServletContainerProvider; + +/** + * Factory class to get all "registered" implementations of {@link ServletContainerProvider}. + * Mentioned implementation classes are registered by {@code META-INF/services}. + * + * @author Libor Kramolis (libor.kramolis at oracle.com) + * @since 2.1 + */ +public final class ServletContainerProviderFactory { + + private ServletContainerProviderFactory() { + } + + /** + * Returns array of all "registered" implementations of {@link ServletContainerProvider}. + * + * @return Array of registered providers. Never returns {@code null}. + * If there is no implementation registered empty array is returned. + */ + public static ServletContainerProvider[] getAllServletContainerProviders() { + // TODO Instances of ServletContainerProvider could be cached, maybe. ??? + // TODO Check if META-INF/services lookup is enabled. ??? + return ServiceFinder.find(ServletContainerProvider.class).toArray(); + } + +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ThreadLocalInvoker.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ThreadLocalInvoker.java new file mode 100644 index 0000000..9ef4724 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ThreadLocalInvoker.java
@@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * A proxy invocation handler that delegates all methods to a thread local instance. + * + * @author Paul Sandoz + */ +public class ThreadLocalInvoker<T> implements InvocationHandler { + + private ThreadLocal<T> threadLocalInstance = new ThreadLocal<>(); + + public void set(final T threadLocalInstance) { + this.threadLocalInstance.set(threadLocalInstance); + } + + public T get() { + return this.threadLocalInstance.get(); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + if (threadLocalInstance.get() == null) { + throw new IllegalStateException(LocalizationMessages.PERSISTENCE_UNIT_NOT_CONFIGURED(proxy.getClass())); + } + + try { + return method.invoke(threadLocalInstance.get(), args); + } catch (final IllegalAccessException ex) { + throw new IllegalStateException(ex); + } catch (final InvocationTargetException ex) { + throw ex.getTargetException(); + } + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ThreadLocalNamedInvoker.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ThreadLocalNamedInvoker.java new file mode 100644 index 0000000..f8acc93 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ThreadLocalNamedInvoker.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import java.lang.reflect.Method; + +import javax.naming.Context; +import javax.naming.InitialContext; + +/** + * A proxy invocation handler that delegates all methods to a thread local instance from JNDI. + * + * @author Paul Sandoz + */ +public class ThreadLocalNamedInvoker<T> extends ThreadLocalInvoker<T> { + + private final String name; + + /** + * Create an instance. + * + * @param name the JNDI name at which an instance of T can be found. + */ + public ThreadLocalNamedInvoker(final String name) { + this.name = name; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // if no instance yet exists for the current thread then look one up and stash it + if (this.get() == null) { + Context ctx = new InitialContext(); + T t = (T) ctx.lookup(name); + this.set(t); + } + return super.invoke(proxy, method, args); + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/Utils.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/Utils.java new file mode 100644 index 0000000..c125116 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/Utils.java
@@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.glassfish.jersey.server.ResourceConfig; + +/** + * Utility class. + * + * @author Michal Gajdos + */ +public final class Utils { + + /** + * Internal {@link javax.servlet.ServletContext servlet context} attribute name under which an instance of + * {@link org.glassfish.jersey.server.ResourceConfig resource config} can be stored. The instance is later used to initialize + * servlet in {@link org.glassfish.jersey.servlet.WebConfig} instead of creating a new one. + */ + private static final String RESOURCE_CONFIG = "jersey.config.servlet.internal.resourceConfig"; + + /** + * Store {@link org.glassfish.jersey.server.ResourceConfig resource config} as an attribute of given + * {@link javax.servlet.ServletContext servlet context}. If {@code config} is {@code null} then the previously stored value + * (if any) is removed. The {@code configName} is used as an attribute name suffix. + * + * @param config resource config to be stored. + * @param context servlet context to store the config in. + * @param configName name or id of the resource config. + */ + public static void store(final ResourceConfig config, final ServletContext context, final String configName) { + final String attributeName = RESOURCE_CONFIG + "_" + configName; + context.setAttribute(attributeName, config); + } + + /** + * Load {@link org.glassfish.jersey.server.ResourceConfig resource config} from given + * {@link javax.servlet.ServletContext servlet context}. If found then the resource config is also removed from servlet + * context. The {@code configName} is used as an attribute name suffix. + * + * @param context servlet context to load resource config from. + * @param configName name or id of the resource config. + * @return previously stored resource config or {@code null} if no resource config has been stored. + */ + public static ResourceConfig retrieve(final ServletContext context, final String configName) { + final String attributeName = RESOURCE_CONFIG + "_" + configName; + final ResourceConfig config = (ResourceConfig) context.getAttribute(attributeName); + context.removeAttribute(attributeName); + return config; + } + + /** + * Extract context params from {@link ServletContext}. + * + * @param servletContext actual servlet context. + * @return map representing current context parameters. + */ + public static Map<String, Object> getContextParams(final ServletContext servletContext) { + final Map<String, Object> props = new HashMap<>(); + final Enumeration names = servletContext.getAttributeNames(); + while (names.hasMoreElements()) { + final String name = (String) names.nextElement(); + props.put(name, servletContext.getAttribute(name)); + } + return props; + } + + /** + * Prevents instantiation. + */ + private Utils() { + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/package-info.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/package-info.java new file mode 100644 index 0000000..6f5d32e --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey internal Servlet API. + */ +package org.glassfish.jersey.servlet.internal;
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/ExtendedServletContainerProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/ExtendedServletContainerProvider.java new file mode 100644 index 0000000..f9fa19b --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/ExtendedServletContainerProvider.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal.spi; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.RequestScopedInitializer; + +/** + * Implementations could provide their own {@link HttpServletRequest} and {@link HttpServletResponse} + * binding implementation in HK2 locator and also an implementation of {@link RequestScopedInitializer} + * that is used to set actual request/response references in injection manager within each request. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @since 2.21 + */ +public interface ExtendedServletContainerProvider extends ServletContainerProvider { + + /** + * Give me a {@link RequestScopedInitializerProvider} instance, that will be utilized + * at runtime to set the actual HTTP Servlet request and response. + * + * The provider returned will be used at runtime for every and each incoming request + * so that the actual request/response instances could be made accessible + * from Jersey injection manager. + * + * @return request scoped initializer provider. + */ + public RequestScopedInitializerProvider getRequestScopedInitializerProvider(); + + /** + * Used by Jersey runtime to tell if the extension covers HTTP Servlet request response + * handling with respect to underlying injection manager. + * + * Return {@code true}, if your implementation configures HK2 bindings + * for {@link HttpServletRequest} and {@link HttpServletResponse} + * in {@link #configure(ResourceConfig)} method + * and also provides a {@link RequestScopedInitializer} implementation + * via {@link #getRequestScopedInitializerProvider()}. + * + * @return {@code true} if the extension fully covers HTTP request/response handling. + */ + public boolean bindsServletRequestResponse(); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/NoOpServletContainerProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/NoOpServletContainerProvider.java new file mode 100644 index 0000000..d4147d8 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/NoOpServletContainerProvider.java
@@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal.spi; + +import java.lang.reflect.Type; +import java.util.Set; + +import javax.ws.rs.core.GenericType; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.server.ResourceConfig; + +/** + * Basic {@link ExtendedServletContainerProvider} that provides + * dummy no-op method implementation. It should be convenient to extend if you only need to implement + * a subset of the original SPI methods. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class NoOpServletContainerProvider implements ExtendedServletContainerProvider { + + public final Type HTTP_SERVLET_REQUEST_TYPE = (new GenericType<Ref<HttpServletRequest>>() { }).getType(); + public final Type HTTP_SERVLET_RESPONSE_TYPE = (new GenericType<Ref<HttpServletResponse>>() { }).getType(); + + @Override + public void preInit(final ServletContext servletContext, final Set<Class<?>> classes) throws ServletException { + // no-op + } + + @Override + public void postInit( + final ServletContext servletContext, final Set<Class<?>> classes, final Set<String> servletNames) { + // no-op + } + + @Override + public void onRegister( + final ServletContext servletContext, final Set<String> servletNames) throws ServletException { + // no-op + } + + @Override + public void configure(final ResourceConfig resourceConfig) throws ServletException { + // no-op + } + + @Override + public boolean bindsServletRequestResponse() { + return false; + } + + @Override + public RequestScopedInitializerProvider getRequestScopedInitializerProvider() { + return null; + } +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/RequestContextProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/RequestContextProvider.java new file mode 100644 index 0000000..49049f2 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/RequestContextProvider.java
@@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal.spi; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Provide access to the actual servlet request/response. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @see {@link RequestScopedInitializerProvider} + */ +public interface RequestContextProvider { + + /** + * Get me the actual HTTP Servlet request. + * + * @return actual HTTP Servlet request. + */ + public HttpServletRequest getHttpServletRequest(); + + /** + * Get me the actual HTTP Servlet response. + * + * @return actual HTTP Servlet response. + */ + public HttpServletResponse getHttpServletResponse(); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/RequestScopedInitializerProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/RequestScopedInitializerProvider.java new file mode 100644 index 0000000..ea18626 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/RequestScopedInitializerProvider.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal.spi; + +import org.glassfish.jersey.server.spi.RequestScopedInitializer; + +/** + * Produces {@link RequestScopedInitializer} + * based on provided {@link RequestContextProvider}. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public interface RequestScopedInitializerProvider { + + /** + * Give me a request scope initializer that could be utilized + * to set the actual Servlet request data in injection manager. + * + * @param context of the actual request. + * @return initializer to be invoked at the start of request processing. + */ + public RequestScopedInitializer get(RequestContextProvider context); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/ServletContainerProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/ServletContainerProvider.java new file mode 100644 index 0000000..2e9b697 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/ServletContainerProvider.java
@@ -0,0 +1,111 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal.spi; + +import java.util.Set; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.RequestScopedInitializer; +import org.glassfish.jersey.servlet.ServletContainer; + +/** + * This is internal Jersey SPI to hook to Jersey servlet initialization process driven by + * {@code org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer}. + * The provider implementation class is registered via {@code META-INF/services}. + * + * + * @author Libor Kramolis (libor.kramolis at oracle.com) + * @since 2.4.1 + */ +public interface ServletContainerProvider { + + /** + * Do your pre-initialization job before Jersey starts its servlet initialization. + * + * It is allowed to configure {@link ServletContext} or add/remove servlet registrations. + * Parameter {@code servletNames} contains list of names of currently registered Jersey servlets. + * + * @param servletContext the {@code ServletContext} of the JAX-RS/Jersey web application that is being started. + * @param classes the mutable Set of application classes that extend {@link javax.ws.rs.core.Application}, + * implement, or have been annotated with the class types {@link javax.ws.rs.Path}, + * {@link javax.ws.rs.ext.Provider} or {@link javax.ws.rs.ApplicationPath}. + * May be empty, never {@code null}. + * @throws ServletException if an error has occurred. {@code javax.servlet.ServletContainerInitializer.onStartup} + * is interrupted. + */ + public void preInit(ServletContext servletContext, Set<Class<?>> classes) throws ServletException; + + /** + * Do your post-initialization job after Jersey finished its servlet initialization. + * + * It is allowed to configure {@link ServletContext} or add/remove servlet registrations. + * Parameter {@code servletNames} contains list of names of currently registered Jersey servlets. + * + * @param servletContext the {@code ServletContext} of the JAX-RS/Jersey web application that is being started. + * @param classes the mutable Set of application classes that extend {@link javax.ws.rs.core.Application}, + * implement, or have been annotated with the class types {@link javax.ws.rs.Path}, + * {@link javax.ws.rs.ext.Provider} or {@link javax.ws.rs.ApplicationPath}. + * May be empty, never {@code null}. + * @param servletNames the Immutable set of Jersey's ServletContainer names. May be empty, never {@code null}. + * @throws ServletException if an error has occurred. {@code javax.servlet.ServletContainerInitializer.onStartup} + * is interrupted. + */ + public void postInit(ServletContext servletContext, Set<Class<?>> classes, final Set<String> servletNames) + throws ServletException; + + /** + * Notifies the provider about all registered Jersey servlets by its names. + * + * It is allowed to configure {@link ServletContext}. Do not add/remove any servlet registrations here. + * + * Parameter {@code servletNames} contains list of names of registered Jersey servlets. + * Currently it is {@link ServletContainer} or + * {@code org.glassfish.jersey.servlet.portability.PortableServletContainer} servlets. + * + * It does not matter servlet container is configured in {@code web.xml}, + * by {@code org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer} + * or by customer direct Servlet API calls. + * + * @param servletContext the {@code ServletContext} of the JAX-RS/Jersey web application that is being started. + * @param servletNames the Immutable set of Jersey's ServletContainer names. May be empty, never {@code null}. + * @throws ServletException if an error has occurred. {@code javax.servlet.ServletContainerInitializer.onStartup} + * is interrupted. + */ + public void onRegister(ServletContext servletContext, final Set<String> servletNames) throws ServletException; + + /** + * This method is called for each {@link ServletContainer} instance initialization, + * i.e. during {@link org.glassfish.jersey.servlet.WebComponent} initialization. + * + * The method is also called during {@link ServletContainer#reload()} or + * {@link ServletContainer#reload(ResourceConfig)} methods invocation. + * + * It does not matter servlet container is configured in {@code web.xml}, + * by {@code org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer} + * or by customer direct Servlet API calls. + * + * @param resourceConfig Jersey application configuration. + * @throws ServletException if an error has occurred. {@code org.glassfish.jersey.servlet.WebComponent} construction + * is interrupted. + */ + public void configure(ResourceConfig resourceConfig) throws ServletException; +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/package-info.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/package-info.java new file mode 100644 index 0000000..e0875ca --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/spi/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey internal Servlet SPI. + */ +package org.glassfish.jersey.servlet.internal.spi;
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/package-info.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/package-info.java new file mode 100644 index 0000000..8e99e29 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey generic Servlet container integration classes. + */ +package org.glassfish.jersey.servlet;
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java new file mode 100644 index 0000000..99a02a8 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
@@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.spi; + +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +/** + * Utilized by the Servlet container response writer to deal with the container async features. + * Individual instances are created by {@link AsyncContextDelegateProvider}. + * + * @see AsyncContextDelegateProvider + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public interface AsyncContextDelegate { + + /** + * Invoked by the superior {@link ContainerResponseWriter} responsible for writing the response when processing is to be + * suspended. An implementation can throw an {@link UnsupportedOperationException} if suspend is not supported (the default + * behavior). + * + * @see ContainerResponseWriter#suspend(long, java.util.concurrent.TimeUnit, org.glassfish.jersey.server.spi.ContainerResponseWriter.TimeoutHandler) + * @throws IllegalStateException if underlying {@link javax.servlet.ServletRequest servlet request} throws an exception. + */ + public void suspend() throws IllegalStateException; + + /** + * Invoked upon a response writing completion when the response write is either committed or canceled. + */ + public void complete(); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegateProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegateProvider.java new file mode 100644 index 0000000..8503ff1 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegateProvider.java
@@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.spi; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Factory to create {@link AsyncContextDelegate} to deal with asynchronous + * features added in Servlet version 3.0. + * If no such a factory is registered via the {@code META-INF/services} mechanism + * a default factory for Servlet versions prior to 3.0 will be utilized with no async support. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public interface AsyncContextDelegateProvider { + + /** + * Factory method to create instances of Servlet container response writer extension, + * {@link AsyncContextDelegate}, for response processing. + * + * @param request original request. + * @param response original response. + * @return an instance to be used throughout a single response write processing. + */ + public AsyncContextDelegate createDelegate(final HttpServletRequest request, final HttpServletResponse response); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/FilterUrlMappingsProvider.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/FilterUrlMappingsProvider.java new file mode 100644 index 0000000..a68b5ea --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/FilterUrlMappingsProvider.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.spi; + +import javax.servlet.FilterConfig; +import java.util.List; + +/** + * Provides an access to context path from the filter configuration. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +public interface FilterUrlMappingsProvider { + + /** + * Return configured context path from the filter configuration. + * + * @param filterConfig the {@link FilterConfig} object + * @returns the {@code List} of url-patterns + */ + List<String> getFilterUrlMappings(final FilterConfig filterConfig); +}
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/package-info.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/package-info.java new file mode 100644 index 0000000..c8a1825 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * SPI for Jersey generic Servlet container support. + */ +package org.glassfish.jersey.servlet.spi;
diff --git a/containers/jersey-servlet-core/src/main/resources/org/glassfish/jersey/servlet/internal/localization.properties b/containers/jersey-servlet-core/src/main/resources/org/glassfish/jersey/servlet/internal/localization.properties new file mode 100644 index 0000000..4250a68 --- /dev/null +++ b/containers/jersey-servlet-core/src/main/resources/org/glassfish/jersey/servlet/internal/localization.properties
@@ -0,0 +1,34 @@ +# +# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +async.processing.not.supported=Asynchronous processing not supported on Servlet 2.x container. +# {0} - status code; {1} - status reason message +exception.sending.error.response=I/O exception occurred while sending "{0}/{1}" error response. +form.param.consumed=A servlet request to the URI {0} contains form parameters in the request body but the request body has been consumed by the servlet or a servlet filter accessing the request parameters. Only resource methods using @FormParam will work as expected. Resource methods consuming the request body by other means will not work as expected. +init.param.regex.syntax.invalid=The syntax is invalid for the regular expression "{0}" associated with the initialization parameter "{1}". +# {0} - name (e.g. 'BookmarkPU') +persistence.unit.not.configured=Persistence unit "{0}" is not configured as a servlet parameter in web.xml. +# {0} - class name +no.thread.local.value=No thread local value in scope for proxy of {0}. +resource.config.parent.class.invalid=Resource configuration class {0} is not a subclass of {1}. +resource.config.unable.to.load=Resource configuration class {0} could not be loaded. +servlet.path.mismatch=The servlet path {0} does not start with the filter context path {1}. +servlet.request.suspend.failed=Attempt to put servlet request into asynchronous mode has failed. Please check your servlet configuration \ + - all Servlet instances and Servlet filters involved in the request processing must explicitly declare support for asynchronous request processing. +header.value.read.failed=Attempt to read the header value failed. +filter.context.path.missing=The root of the app was not properly defined. Either use a Servlet 3.x container or add \ + an init-param 'jersey.config.servlet.filter.contextPath' to the filter configuration. Due to Servlet 2.x API, Jersey cannot \ + determine the request base URI solely from the ServletContext. The application will most likely not work.
diff --git a/containers/jersey-servlet-core/src/test/java/org/glassfish/jersey/servlet/internal/ThreadLocalInvokerTest.java b/containers/jersey-servlet-core/src/test/java/org/glassfish/jersey/servlet/internal/ThreadLocalInvokerTest.java new file mode 100644 index 0000000..b4476fc --- /dev/null +++ b/containers/jersey-servlet-core/src/test/java/org/glassfish/jersey/servlet/internal/ThreadLocalInvokerTest.java
@@ -0,0 +1,100 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.internal; + +import java.lang.reflect.Proxy; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Michal Gajdos + */ +public class ThreadLocalInvokerTest { + + public static class CheckedException extends Exception { + + } + + public static interface X { + + public String checked() throws CheckedException; + + public String runtime(); + } + + @Test + public void testIllegalState() { + final ThreadLocalInvoker<X> tli = new ThreadLocalInvoker<>(); + + final X x = (X) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{X.class}, tli); + + boolean caught = false; + try { + x.checked(); + } catch (final Exception ex) { + caught = true; + assertEquals(IllegalStateException.class, ex.getClass()); + } + assertTrue(caught); + + caught = false; + try { + x.runtime(); + } catch (final Exception ex) { + caught = true; + assertEquals(IllegalStateException.class, ex.getClass()); + } + assertTrue(caught); + } + + @Test + public void testExceptions() { + final ThreadLocalInvoker<X> tli = new ThreadLocalInvoker<>(); + + final X x = (X) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{X.class}, tli); + + tli.set(new X() { + public String checked() throws CheckedException { + throw new CheckedException(); + } + + public String runtime() { + throw new RuntimeException(); + } + }); + + boolean caught = false; + try { + x.checked(); + } catch (final Exception ex) { + caught = true; + assertEquals(CheckedException.class, ex.getClass()); + } + assertTrue(caught); + + caught = false; + try { + x.runtime(); + } catch (final Exception ex) { + caught = true; + assertEquals(RuntimeException.class, ex.getClass()); + } + assertTrue(caught); + } +}
diff --git a/containers/jersey-servlet/pom.xml b/containers/jersey-servlet/pom.xml new file mode 100644 index 0000000..a9a6634 --- /dev/null +++ b/containers/jersey-servlet/pom.xml
@@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-servlet</artifactId> + <packaging>jar</packaging> + <name>jersey-container-servlet</name> + + <description>Jersey core Servlet 3.x implementation</description> + + <dependencies> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <version>${servlet3.version}</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${project.version}</version> + <exclusions> + <exclusion> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- Note: When you're changing these properties change them also in bundles/jax-rs-ri/bundle/pom.xml. --> + <Import-Package> + javax.servlet.*;version="[3.0,5.0)", + javax.annotation.*;version=!, + * + </Import-Package> + </instructions> + <unpackBundle>true</unpackBundle> + </configuration> + </plugin> + </plugins> + </build> + +</project>
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java new file mode 100644 index 0000000..25ad283 --- /dev/null +++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.async; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.AsyncContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.servlet.init.internal.LocalizationMessages; +import org.glassfish.jersey.servlet.spi.AsyncContextDelegate; +import org.glassfish.jersey.servlet.spi.AsyncContextDelegateProvider; + +/** + * Servlet 3.x container response writer async extension and related extension factory implementation. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class AsyncContextDelegateProviderImpl implements AsyncContextDelegateProvider { + + private static final Logger LOGGER = Logger.getLogger(AsyncContextDelegateProviderImpl.class.getName()); + + @Override + public final AsyncContextDelegate createDelegate(final HttpServletRequest request, final HttpServletResponse response) { + return new ExtensionImpl(request, response); + } + + private static final class ExtensionImpl implements AsyncContextDelegate { + + private static final int NEVER_TIMEOUT_VALUE = -1; + + private final HttpServletRequest request; + private final HttpServletResponse response; + private final AtomicReference<AsyncContext> asyncContextRef; + private final AtomicBoolean completed; + + /** + * Create a Servlet 3.x {@link AsyncContextDelegate} with given {@code request} and {@code response}. + * + * @param request request to create {@link AsyncContext} for. + * @param response response to create {@link AsyncContext} for. + */ + private ExtensionImpl(final HttpServletRequest request, final HttpServletResponse response) { + this.request = request; + this.response = response; + this.asyncContextRef = new AtomicReference<>(); + this.completed = new AtomicBoolean(false); + } + + @Override + public void suspend() throws IllegalStateException { + // Suspend only if not completed and not suspended before. + if (!completed.get() && asyncContextRef.get() == null) { + asyncContextRef.set(getAsyncContext()); + } + } + + private AsyncContext getAsyncContext() { + final AsyncContext asyncContext; + if (request.isAsyncStarted()) { + asyncContext = request.getAsyncContext(); + try { + asyncContext.setTimeout(NEVER_TIMEOUT_VALUE); + } catch (IllegalStateException ex) { + // Let's hope the time out is set properly, otherwise JAX-RS AsyncResponse time-out support + // may not work as expected... At least we can log this at fine level... + LOGGER.log(Level.FINE, LocalizationMessages.SERVLET_ASYNC_CONTEXT_ALREADY_STARTED(), ex); + } + } else { + asyncContext = request.startAsync(request, response); + // Tell underlying asyncContext to never time out. + asyncContext.setTimeout(NEVER_TIMEOUT_VALUE); + } + return asyncContext; + } + + @Override + public void complete() { + completed.set(true); + + final AsyncContext asyncContext = asyncContextRef.getAndSet(null); + if (asyncContext != null) { + asyncContext.complete(); + } + } + } +}
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/package-info.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/package-info.java new file mode 100644 index 0000000..0e02af9 --- /dev/null +++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey servlet container asynchronous support classes. + */ +package org.glassfish.jersey.servlet.async;
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/FilterUrlMappingsProviderImpl.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/FilterUrlMappingsProviderImpl.java new file mode 100644 index 0000000..e632f72 --- /dev/null +++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/FilterUrlMappingsProviderImpl.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.init; + +import org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider; + +import javax.servlet.FilterConfig; +import javax.servlet.FilterRegistration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Provide all configured context paths (url mappings) of the application deployed using filter. + * <p> + * The url patterns are returned without the eventual trailing asterisk. + * <p> + * The functionality is available in Servlet 3.x environment only, so this + * implementation of {@link FilterUrlMappingsProvider} interface is Servlet 3 specific. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +public class FilterUrlMappingsProviderImpl implements FilterUrlMappingsProvider { + @Override + public List<String> getFilterUrlMappings(FilterConfig filterConfig) { + FilterRegistration filterRegistration = + filterConfig.getServletContext().getFilterRegistration(filterConfig.getFilterName()); + + Collection<String> urlPatternMappings = filterRegistration.getUrlPatternMappings(); + List<String> result = new ArrayList<>(); + + for (String pattern : urlPatternMappings) { + result.add(pattern.endsWith("*") ? pattern.substring(0, pattern.length() - 1) : pattern); + } + + return result; + } +}
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/JerseyServletContainerInitializer.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/JerseyServletContainerInitializer.java new file mode 100644 index 0000000..5d58730 --- /dev/null +++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/JerseyServletContainerInitializer.java
@@ -0,0 +1,378 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.servlet.init; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.ext.Provider; + +import javax.servlet.Registration; +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.annotation.HandlesTypes; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.servlet.init.internal.LocalizationMessages; +import org.glassfish.jersey.servlet.internal.ServletContainerProviderFactory; +import org.glassfish.jersey.servlet.internal.Utils; +import org.glassfish.jersey.servlet.internal.spi.ServletContainerProvider; + +/* + It is RECOMMENDED that implementations support the Servlet 3 framework + pluggability mechanism to enable portability between containers and to avail + themselves of container-supplied class scanning facilities. + When using the pluggability mechanism the following conditions MUST be met: + + - If no Application subclass is present the added servlet MUST be + named "javax.ws.rs.core.Application" and all root resource classes and + providers packaged in the web application MUST be included in the published + JAX-RS application. The application MUST be packaged with a web.xml that + specifies a servlet mapping for the added servlet. + + - If an Application subclass is present and there is already a servlet defined + that has a servlet initialization parameter named "javax.ws.rs.Application" + whose value is the fully qualified name of the Application subclass then no + servlet should be added by the JAX-RS implementation's ContainerInitializer + since the application is already being handled by an existing servlet. + + - If an application subclass is present that is not being handled by an + existing servlet then the servlet added by the ContainerInitializer MUST be + named with the fully qualified name of the Application subclass. If the + Application subclass is annotated with @PathPrefix and no servlet-mapping + exists for the added servlet then a new servlet mapping is added with the + value of the @PathPrefix annotation with "/*" appended otherwise the existing + mapping is used. If the Application subclass is not annotated with @PathPrefix + then the application MUST be packaged with a web.xml that specifies a servlet + mapping for the added servlet. It is an error for more than one Application + to be deployed at the same effective servlet mapping. + + In either of the latter two cases, if both Application#getClasses and + Application#getSingletons return an empty list then all root resource classes + and providers packaged in the web application MUST be included in the + published JAX-RS application. If either getClasses or getSingletons return a + non-empty list then only those classes or singletons returned MUST be included + in the published JAX-RS application. + + If not using the Servlet 3 framework pluggability mechanism + (e.g. in a pre-Servlet 3.0 container), the servlet-class or filter-class + element of the web.xml descriptor SHOULD name the JAX-RS + implementation-supplied Servlet or Filter class respectively. The + application-supplied subclass of Application SHOULD be identified using an + init-param with a param-name of javax.ws.rs.Application. + */ + +/** + * {@link ServletContainerInitializer} implementation used for Servlet 3.x deployment. + * + * @author Paul Sandoz + * @author Martin Matula + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +@HandlesTypes({ Path.class, Provider.class, Application.class, ApplicationPath.class }) +public final class JerseyServletContainerInitializer implements ServletContainerInitializer { + + private static final Logger LOGGER = Logger.getLogger(JerseyServletContainerInitializer.class.getName()); + + @Override + public void onStartup(Set<Class<?>> classes, final ServletContext servletContext) throws ServletException { + final ServletContainerProvider[] allServletContainerProviders = + ServletContainerProviderFactory.getAllServletContainerProviders(); + + if (classes == null) { + classes = Collections.emptySet(); + } + // PRE INIT + for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) { + servletContainerProvider.preInit(servletContext, classes); + } + // INIT IMPL + onStartupImpl(classes, servletContext); + // POST INIT + for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) { + servletContainerProvider.postInit(servletContext, classes, findJerseyServletNames(servletContext)); + } + // ON REGISTER + for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) { + servletContainerProvider.onRegister(servletContext, findJerseyServletNames(servletContext)); + } + } + + private void onStartupImpl(final Set<Class<?>> classes, final ServletContext servletContext) throws ServletException { + // first see if there are any application classes in the web app + for (final Class<? extends Application> applicationClass : getApplicationClasses(classes)) { + final ServletRegistration servletRegistration = servletContext.getServletRegistration(applicationClass.getName()); + + if (servletRegistration != null) { + addServletWithExistingRegistration(servletContext, servletRegistration, applicationClass, classes); + } else { + // Servlet is not registered with app name or the app name is used to register a different servlet + // check if some servlet defines the app in init params + final List<Registration> srs = getInitParamDeclaredRegistrations(servletContext, applicationClass); + if (!srs.isEmpty()) { + // app handled by at least one servlet or filter + // fix the registrations if needed (i.e. add servlet class) + for (final Registration sr : srs) { + if (sr instanceof ServletRegistration) { + addServletWithExistingRegistration(servletContext, (ServletRegistration) sr, + applicationClass, classes); + } + } + } else { + // app not handled by any servlet/filter -> add it + addServletWithApplication(servletContext, applicationClass, classes); + } + } + } + + // check for javax.ws.rs.core.Application registration + addServletWithDefaultConfiguration(servletContext, classes); + } + + /** + * Returns names of all registered Jersey servlets. + * + * Servlets are configured in {@code web.xml} or managed via Servlet API. + * + * @param servletContext the {@link ServletContext} of the web application that is being started + * @return list of Jersey servlet names or empty array, never returns {@code null} + */ + private static Set<String> findJerseyServletNames(final ServletContext servletContext) { + final Set<String> jerseyServletNames = new HashSet<>(); + + for (final ServletRegistration servletRegistration : servletContext.getServletRegistrations().values()) { + if (isJerseyServlet(servletRegistration.getClassName())) { + jerseyServletNames.add(servletRegistration.getName()); + } + } + return Collections.unmodifiableSet(jerseyServletNames); + } + + /** + * Check if the {@code className} is an implementation of a Jersey Servlet container. + * + * @return {@code true} if the class is a Jersey servlet container class, {@code false} otherwise. + */ + private static boolean isJerseyServlet(final String className) { + return ServletContainer.class.getName().equals(className) + || "org.glassfish.jersey.servlet.portability.PortableServletContainer".equals(className); + } + + private static List<Registration> getInitParamDeclaredRegistrations(final ServletContext context, + final Class<? extends Application> clazz) { + final List<Registration> registrations = new ArrayList<>(); + collectJaxRsRegistrations(context.getServletRegistrations(), registrations, clazz); + collectJaxRsRegistrations(context.getFilterRegistrations(), registrations, clazz); + return registrations; + } + + private static void collectJaxRsRegistrations(final Map<String, ? extends Registration> registrations, + final List<Registration> collected, final Class<? extends Application> a) { + for (final Registration sr : registrations.values()) { + final Map<String, String> ips = sr.getInitParameters(); + if (ips.containsKey(ServletProperties.JAXRS_APPLICATION_CLASS)) { + if (ips.get(ServletProperties.JAXRS_APPLICATION_CLASS).equals(a.getName())) { + collected.add(sr); + } + } + } + } + + /** + * Enhance default servlet (named {@link Application}) configuration. + */ + private static void addServletWithDefaultConfiguration(final ServletContext context, + final Set<Class<?>> classes) throws ServletException { + + ServletRegistration registration = context.getServletRegistration(Application.class.getName()); + + if (registration != null) { + final Set<Class<?>> appClasses = getRootResourceAndProviderClasses(classes); + final ResourceConfig resourceConfig = ResourceConfig.forApplicationClass(ResourceConfig.class, appClasses) + .addProperties(getInitParams(registration)) + .addProperties(Utils.getContextParams(context)); + + if (registration.getClassName() != null) { + // class name present - complete servlet registration from container point of view + Utils.store(resourceConfig, context, registration.getName()); + } else { + // no class name - no complete servlet registration from container point of view + final ServletContainer servlet = new ServletContainer(resourceConfig); + registration = context.addServlet(registration.getName(), servlet); + ((ServletRegistration.Dynamic) registration).setLoadOnStartup(1); + + if (registration.getMappings().isEmpty()) { + // Error + LOGGER.log(Level.WARNING, LocalizationMessages.JERSEY_APP_NO_MAPPING(registration.getName())); + } else { + LOGGER.log(Level.CONFIG, + LocalizationMessages.JERSEY_APP_REGISTERED_CLASSES(registration.getName(), appClasses)); + } + } + } + } + + /** + * Add new servlet according to {@link Application} subclass with {@link ApplicationPath} annotation or existing + * {@code servlet-mapping}. + */ + private static void addServletWithApplication(final ServletContext context, + final Class<? extends Application> clazz, + final Set<Class<?>> defaultClasses) throws ServletException { + final ApplicationPath ap = clazz.getAnnotation(ApplicationPath.class); + if (ap != null) { + // App is annotated with ApplicationPath + final ResourceConfig resourceConfig = ResourceConfig.forApplicationClass(clazz, defaultClasses) + .addProperties(Utils.getContextParams(context)); + final ServletContainer s = new ServletContainer(resourceConfig); + final ServletRegistration.Dynamic dsr = context.addServlet(clazz.getName(), s); + dsr.setAsyncSupported(true); + dsr.setLoadOnStartup(1); + + final String mapping = createMappingPath(ap); + if (!mappingExists(context, mapping)) { + dsr.addMapping(mapping); + + LOGGER.log(Level.CONFIG, LocalizationMessages.JERSEY_APP_REGISTERED_MAPPING(clazz.getName(), mapping)); + } else { + LOGGER.log(Level.WARNING, LocalizationMessages.JERSEY_APP_MAPPING_CONFLICT(clazz.getName(), mapping)); + } + } + } + + /** + * Enhance existing servlet configuration. + */ + private static void addServletWithExistingRegistration(final ServletContext context, + ServletRegistration registration, + final Class<? extends Application> clazz, + final Set<Class<?>> classes) throws ServletException { + // create a new servlet container for a given app. + final ResourceConfig resourceConfig = ResourceConfig.forApplicationClass(clazz, classes) + .addProperties(getInitParams(registration)) + .addProperties(Utils.getContextParams(context)); + + if (registration.getClassName() != null) { + // class name present - complete servlet registration from container point of view + Utils.store(resourceConfig, context, registration.getName()); + } else { + // no class name - no complete servlet registration from container point of view + final ServletContainer servlet = new ServletContainer(resourceConfig); + final ServletRegistration.Dynamic dynamicRegistration = context.addServlet(clazz.getName(), servlet); + dynamicRegistration.setAsyncSupported(true); + dynamicRegistration.setLoadOnStartup(1); + registration = dynamicRegistration; + } + if (registration.getMappings().isEmpty()) { + final ApplicationPath ap = clazz.getAnnotation(ApplicationPath.class); + if (ap != null) { + final String mapping = createMappingPath(ap); + if (!mappingExists(context, mapping)) { + registration.addMapping(mapping); + + LOGGER.log(Level.CONFIG, LocalizationMessages.JERSEY_APP_REGISTERED_MAPPING(clazz.getName(), mapping)); + } else { + LOGGER.log(Level.WARNING, LocalizationMessages.JERSEY_APP_MAPPING_CONFLICT(clazz.getName(), mapping)); + } + } else { + // Error + LOGGER.log(Level.WARNING, LocalizationMessages.JERSEY_APP_NO_MAPPING_OR_ANNOTATION(clazz.getName(), + ApplicationPath.class.getSimpleName())); + } + } else { + LOGGER.log(Level.CONFIG, LocalizationMessages.JERSEY_APP_REGISTERED_APPLICATION(clazz.getName())); + } + } + + private static Map<String, Object> getInitParams(final ServletRegistration sr) { + final Map<String, Object> initParams = new HashMap<>(); + for (final Map.Entry<String, String> entry : sr.getInitParameters().entrySet()) { + initParams.put(entry.getKey(), entry.getValue()); + } + return initParams; + } + + private static boolean mappingExists(final ServletContext sc, final String mapping) { + for (final ServletRegistration sr : sc.getServletRegistrations().values()) { + for (final String declaredMapping : sr.getMappings()) { + if (mapping.equals(declaredMapping)) { + return true; + } + } + } + + return false; + } + + + + private static String createMappingPath(final ApplicationPath ap) { + String path = ap.value(); + if (!path.startsWith("/")) { + path = "/" + path; + } + + if (!path.endsWith("/*")) { + if (path.endsWith("/")) { + path += "*"; + } else { + path += "/*"; + } + } + + return path; + } + + private static Set<Class<? extends Application>> getApplicationClasses(final Set<Class<?>> classes) { + final Set<Class<? extends Application>> s = new LinkedHashSet<>(); + for (final Class<?> c : classes) { + if (Application.class != c && Application.class.isAssignableFrom(c)) { + s.add(c.asSubclass(Application.class)); + } + } + + return s; + } + + private static Set<Class<?>> getRootResourceAndProviderClasses(final Set<Class<?>> classes) { + // TODO filter out any classes from the Jersey jars + final Set<Class<?>> s = new LinkedHashSet<>(); + for (final Class<?> c : classes) { + if (c.isAnnotationPresent(Path.class) || c.isAnnotationPresent(Provider.class)) { + s.add(c); + } + } + + return s; + } + +}
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/package-info.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/package-info.java new file mode 100644 index 0000000..c19811d --- /dev/null +++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/init/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey servlet container initialization classes. + */ +package org.glassfish.jersey.servlet.init;
diff --git a/containers/jersey-servlet/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/containers/jersey-servlet/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer new file mode 100644 index 0000000..4052e74 --- /dev/null +++ b/containers/jersey-servlet/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer
@@ -0,0 +1 @@ +org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer \ No newline at end of file
diff --git a/containers/jersey-servlet/src/main/resources/META-INF/services/org.glassfish.jersey.servlet.spi.AsyncContextDelegateProvider b/containers/jersey-servlet/src/main/resources/META-INF/services/org.glassfish.jersey.servlet.spi.AsyncContextDelegateProvider new file mode 100644 index 0000000..959ec7c --- /dev/null +++ b/containers/jersey-servlet/src/main/resources/META-INF/services/org.glassfish.jersey.servlet.spi.AsyncContextDelegateProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl \ No newline at end of file
diff --git a/containers/jersey-servlet/src/main/resources/META-INF/services/org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider b/containers/jersey-servlet/src/main/resources/META-INF/services/org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider new file mode 100644 index 0000000..6f15689 --- /dev/null +++ b/containers/jersey-servlet/src/main/resources/META-INF/services/org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.servlet.init.FilterUrlMappingsProviderImpl \ No newline at end of file
diff --git a/containers/jersey-servlet/src/main/resources/org/glassfish/jersey/servlet/init/internal/localization.properties b/containers/jersey-servlet/src/main/resources/org/glassfish/jersey/servlet/init/internal/localization.properties new file mode 100644 index 0000000..6f1b074 --- /dev/null +++ b/containers/jersey-servlet/src/main/resources/org/glassfish/jersey/servlet/init/internal/localization.properties
@@ -0,0 +1,24 @@ +# +# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +jersey.app.mapping.conflict=Mapping conflict. A Servlet registration exists with same mapping as the Jersey servlet application, named {0}, at the servlet mapping, {1}. +jersey.app.no.mapping=The Jersey servlet application, named {0}, has no servlet mapping. +jersey.app.no.mapping.or.annotation=The Jersey servlet application, named {0}, is not annotated with {1} and has no servlet mapping. +jersey.app.registered.classes=Registering the Jersey servlet application, named {0}, with the following root resource and provider classes: {1} +jersey.app.registered.mapping=Registering the Jersey servlet application, named {0}, at the servlet mapping {1}, with the Application class of the same name. +jersey.app.registered.application=Registering the Jersey servlet application, named {0}, with the Application class of the same name. +servlet.async.context.already.started=Servlet request has been put into asynchronous mode by an external force. \ + Proceeding with the existing AsyncContext instance, but cannot guarantee the correct behavior of JAX-RS AsyncResponse time-out support.
diff --git a/containers/jetty-http/pom.xml b/containers/jetty-http/pom.xml new file mode 100644 index 0000000..eb3d0b8 --- /dev/null +++ b/containers/jetty-http/pom.xml
@@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>project</artifactId> + <groupId>org.glassfish.jersey.containers</groupId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-jetty-http</artifactId> + <packaging>jar</packaging> + <name>jersey-container-jetty-http</name> + + <description>Jetty Http Container</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-util</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-continuation</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + + </plugins> + + <resources> + <resource> + <directory>${basedir}/src/main/java</directory> + <includes> + <include>META-INF/**/*</include> + </includes> + </resource> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> + + <profiles> + <profile> + <id>testsSkipJdk6</id> + <activation> + <jdk>1.6</jdk> + </activation> + <properties> + <skip.tests>true</skip.tests> + </properties> + </profile> + </profiles> + +</project>
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java new file mode 100644 index 0000000..fa93775 --- /dev/null +++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java
@@ -0,0 +1,459 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.SecurityContext; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.ExtendedLogger; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.jetty.internal.LocalizationMessages; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.internal.ContainerUtils; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import org.eclipse.jetty.continuation.Continuation; +import org.eclipse.jetty.continuation.ContinuationListener; +import org.eclipse.jetty.continuation.ContinuationSupport; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.AbstractHandler; + +/** + * Jersey {@code Container} implementation based on Jetty {@link org.eclipse.jetty.server.Handler}. + * + * @author Arul Dhesiaseelan (aruld@acm.org) + * @author Libor Kramolis (libor.kramolis at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class JettyHttpContainer extends AbstractHandler implements Container { + + private static final ExtendedLogger LOGGER = + new ExtendedLogger(Logger.getLogger(JettyHttpContainer.class.getName()), Level.FINEST); + + private static final Type REQUEST_TYPE = (new GenericType<Ref<Request>>() {}).getType(); + private static final Type RESPONSE_TYPE = (new GenericType<Ref<Response>>() {}).getType(); + + private static final int INTERNAL_SERVER_ERROR = javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + + /** + * Cached value of configuration property + * {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR}. + * If {@code true} method {@link HttpServletResponse#setStatus} is used over {@link HttpServletResponse#sendError}. + */ + private boolean configSetStatusOverSendError; + + /** + * Referencing factory for Jetty request. + */ + private static class JettyRequestReferencingFactory extends ReferencingFactory<Request> { + @Inject + public JettyRequestReferencingFactory(final Provider<Ref<Request>> referenceFactory) { + super(referenceFactory); + } + } + + /** + * Referencing factory for Jetty response. + */ + private static class JettyResponseReferencingFactory extends ReferencingFactory<Response> { + @Inject + public JettyResponseReferencingFactory(final Provider<Ref<Response>> referenceFactory) { + super(referenceFactory); + } + } + + /** + * An internal binder to enable Jetty HTTP container specific types injection. + * This binder allows to inject underlying Jetty HTTP request and response instances. + * Note that since Jetty {@code Request} class is not proxiable as it does not expose an empty constructor, + * the injection of Jetty request instance into singleton JAX-RS and Jersey providers is only supported via + * {@link javax.inject.Provider injection provider}. + */ + private static class JettyBinder extends AbstractBinder { + + @Override + protected void configure() { + bindFactory(JettyRequestReferencingFactory.class).to(Request.class) + .proxy(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.<Request>referenceFactory()).to(new GenericType<Ref<Request>>() {}) + .in(RequestScoped.class); + + bindFactory(JettyResponseReferencingFactory.class).to(Response.class) + .proxy(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.<Response>referenceFactory()).to(new GenericType<Ref<Response>>() {}) + .in(RequestScoped.class); + } + } + + private volatile ApplicationHandler appHandler; + + @Override + public void handle(final String target, final Request request, final HttpServletRequest httpServletRequest, + final HttpServletResponse httpServletResponse) throws IOException, ServletException { + + final Response response = request.getResponse(); + final ResponseWriter responseWriter = new ResponseWriter(request, response, configSetStatusOverSendError); + final URI baseUri = getBaseUri(request); + final URI requestUri = getRequestUri(request, baseUri); + try { + final ContainerRequest requestContext = new ContainerRequest( + baseUri, + requestUri, + request.getMethod(), + getSecurityContext(request), + new MapPropertiesDelegate()); + requestContext.setEntityStream(request.getInputStream()); + final Enumeration<String> headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + final String headerName = headerNames.nextElement(); + String headerValue = request.getHeader(headerName); + requestContext.headers(headerName, headerValue == null ? "" : headerValue); + } + requestContext.setWriter(responseWriter); + requestContext.setRequestScopedInitializer(injectionManager -> { + injectionManager.<Ref<Request>>getInstance(REQUEST_TYPE).set(request); + injectionManager.<Ref<Response>>getInstance(RESPONSE_TYPE).set(response); + }); + + // Mark the request as handled before generating the body of the response + request.setHandled(true); + appHandler.handle(requestContext); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + + } + + private URI getRequestUri(final Request request, final URI baseUri) { + try { + final String serverAddress = getServerAddress(baseUri); + String uri = request.getRequestURI(); + + final String queryString = request.getQueryString(); + if (queryString != null) { + uri = uri + "?" + ContainerUtils.encodeUnsafeCharacters(queryString); + } + + return new URI(serverAddress + uri); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private String getServerAddress(URI baseUri) { + String serverAddress = baseUri.toString(); + if (serverAddress.charAt(serverAddress.length() - 1) == '/') { + return serverAddress.substring(0, serverAddress.length() - 1); + } + return serverAddress; + } + + private SecurityContext getSecurityContext(final Request request) { + return new SecurityContext() { + + @Override + public boolean isUserInRole(final String role) { + return request.isUserInRole(role); + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public Principal getUserPrincipal() { + return request.getUserPrincipal(); + } + + @Override + public String getAuthenticationScheme() { + return request.getAuthType(); + } + }; + } + + + private URI getBaseUri(final Request request) { + try { + return new URI(request.getScheme(), null, request.getServerName(), + request.getServerPort(), getBasePath(request), null, null); + } catch (final URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private String getBasePath(final Request request) { + final String contextPath = request.getContextPath(); + + if (contextPath == null || contextPath.isEmpty()) { + return "/"; + } else if (contextPath.charAt(contextPath.length() - 1) != '/') { + return contextPath + "/"; + } else { + return contextPath; + } + } + + private static final class ResponseWriter implements ContainerResponseWriter { + + private final Response response; + private final Continuation continuation; + private final boolean configSetStatusOverSendError; + + ResponseWriter(final Request request, final Response response, final boolean configSetStatusOverSendError) { + this.response = response; + this.continuation = ContinuationSupport.getContinuation(request); + this.configSetStatusOverSendError = configSetStatusOverSendError; + } + + @Override + public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context) + throws ContainerException { + + final javax.ws.rs.core.Response.StatusType statusInfo = context.getStatusInfo(); + + final int code = statusInfo.getStatusCode(); + final String reason = statusInfo.getReasonPhrase() == null + ? HttpStatus.getMessage(code) : statusInfo.getReasonPhrase(); + + response.setStatusWithReason(code, reason); + + if (contentLength != -1 && contentLength < Integer.MAX_VALUE) { + response.setContentLength((int) contentLength); + } + for (final Map.Entry<String, List<String>> e : context.getStringHeaders().entrySet()) { + for (final String value : e.getValue()) { + response.addHeader(e.getKey(), value); + } + } + + try { + return response.getOutputStream(); + } catch (final IOException ioe) { + throw new ContainerException("Error during writing out the response headers.", ioe); + } + } + + @Override + public boolean suspend(final long timeOut, final TimeUnit timeUnit, final TimeoutHandler timeoutHandler) { + try { + if (timeOut > 0) { + final long timeoutMillis = TimeUnit.MILLISECONDS.convert(timeOut, timeUnit); + continuation.setTimeout(timeoutMillis); + } + continuation.addContinuationListener(new ContinuationListener() { + @Override + public void onComplete(final Continuation continuation) { + } + + @Override + public void onTimeout(final Continuation continuation) { + if (timeoutHandler != null) { + timeoutHandler.onTimeout(ResponseWriter.this); + } + } + }); + continuation.suspend(response); + return true; + } catch (final Exception ex) { + return false; + } + } + + @Override + public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throws IllegalStateException { + if (timeOut > 0) { + final long timeoutMillis = TimeUnit.MILLISECONDS.convert(timeOut, timeUnit); + continuation.setTimeout(timeoutMillis); + } + } + + @Override + public void commit() { + try { + response.closeOutput(); + } catch (final IOException e) { + LOGGER.log(Level.WARNING, LocalizationMessages.UNABLE_TO_CLOSE_RESPONSE(), e); + } finally { + if (continuation.isSuspended()) { + continuation.complete(); + } + LOGGER.log(Level.FINEST, "commit() called"); + } + } + + @Override + public void failure(final Throwable error) { + try { + if (!response.isCommitted()) { + try { + if (configSetStatusOverSendError) { + response.reset(); + //noinspection deprecation + response.setStatus(INTERNAL_SERVER_ERROR, "Request failed."); + } else { + response.sendError(INTERNAL_SERVER_ERROR, "Request failed."); + } + } catch (final IllegalStateException ex) { + // a race condition externally committing the response can still occur... + LOGGER.log(Level.FINER, "Unable to reset failed response.", ex); + } catch (final IOException ex) { + throw new ContainerException(LocalizationMessages.EXCEPTION_SENDING_ERROR_RESPONSE(INTERNAL_SERVER_ERROR, + "Request failed."), ex); + } + } + } finally { + LOGGER.log(Level.FINEST, "failure(...) called"); + commit(); + rethrow(error); + } + } + + @Override + public boolean enableResponseBuffering() { + return false; + } + + /** + * Rethrow the original exception as required by JAX-RS, 3.3.4. + * + * @param error throwable to be re-thrown + */ + private void rethrow(final Throwable error) { + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new ContainerException(error); + } + } + + } + + @Override + public ResourceConfig getConfiguration() { + return appHandler.getConfiguration(); + } + + @Override + public void reload() { + reload(getConfiguration()); + } + + @Override + public void reload(final ResourceConfig configuration) { + appHandler.onShutdown(this); + + appHandler = new ApplicationHandler(configuration.register(new JettyBinder())); + appHandler.onReload(this); + appHandler.onStartup(this); + cacheConfigSetStatusOverSendError(); + } + + @Override + public ApplicationHandler getApplicationHandler() { + return appHandler; + } + + /** + * Inform this container that the server has been started. + * This method must be implicitly called after the server containing this container is started. + * + * @throws java.lang.Exception if a problem occurred during server startup. + */ + @Override + protected void doStart() throws Exception { + super.doStart(); + appHandler.onStartup(this); + } + + /** + * Inform this container that the server is being stopped. + * This method must be implicitly called before the server containing this container is stopped. + * + * @throws java.lang.Exception if a problem occurred during server shutdown. + */ + @Override + public void doStop() throws Exception { + super.doStop(); + appHandler.onShutdown(this); + appHandler = null; + } + + /** + * Create a new Jetty HTTP container. + * + * @param application JAX-RS / Jersey application to be deployed on Jetty HTTP container. + * @param parentContext DI provider specific context with application's registered bindings. + */ + JettyHttpContainer(final Application application, final Object parentContext) { + this.appHandler = new ApplicationHandler(application, new JettyBinder(), parentContext); + } + + /** + * Create a new Jetty HTTP container. + * + * @param application JAX-RS / Jersey application to be deployed on Jetty HTTP container. + */ + JettyHttpContainer(final Application application) { + this.appHandler = new ApplicationHandler(application, new JettyBinder()); + + cacheConfigSetStatusOverSendError(); + } + + /** + * The method reads and caches value of configuration property + * {@link ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR} for future purposes. + */ + private void cacheConfigSetStatusOverSendError() { + this.configSetStatusOverSendError = ServerProperties.getValue(getConfiguration().getProperties(), + ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, false, Boolean.class); + } + +}
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java new file mode 100644 index 0000000..8155ea7 --- /dev/null +++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
@@ -0,0 +1,300 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import java.net.URI; +import java.util.concurrent.ThreadFactory; + +import javax.ws.rs.ProcessingException; + +import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder; +import org.glassfish.jersey.jetty.internal.LocalizationMessages; +import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler; +import org.glassfish.jersey.server.ContainerFactory; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +/** + * Factory for creating and starting Jetty server handlers. This returns + * a handle to the started server as {@link Server} instances, which allows + * the server to be stopped by invoking the {@link org.eclipse.jetty.server.Server#stop()} method. + * <p/> + * To start the server in HTTPS mode an {@link SslContextFactory} can be provided. + * This will be used to decrypt and encrypt information sent over the + * connected TCP socket channel. + * + * @author Arul Dhesiaseelan (aruld@acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class JettyHttpContainerFactory { + + private JettyHttpContainerFactory() { + } + + /** + * Creates a {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}. + * + * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path + * segment will be used as context path, the rest will be ignored. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + */ + public static Server createServer(final URI uri) throws ProcessingException { + return createServer(uri, null, null, true); + } + + /** + * Creates a {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}. + * + * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path + * segment will be used as context path, the rest will be ignored. + * @param start if set to false, server will not get started, which allows to configure the underlying transport + * layer, see above for details. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + */ + public static Server createServer(final URI uri, final boolean start) throws ProcessingException { + return createServer(uri, null, null, start); + } + + /** + * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that + * in turn manages all root resource and provider classes declared by the + * resource configuration. + * <p/> + * This implementation defers to the + * {@link org.glassfish.jersey.server.ContainerFactory#createContainer(Class, javax.ws.rs.core.Application)} method + * for creating an Container that manages the root resources. + * + * @param uri the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI path, query and fragment components are ignored. + * @param config the resource configuration. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + */ + public static Server createServer(final URI uri, final ResourceConfig config) + throws ProcessingException { + + final JettyHttpContainer container = ContainerFactory.createContainer(JettyHttpContainer.class, config); + return createServer(uri, null, container, true); + } + + /** + * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that + * in turn manages all root resource and provider classes declared by the + * resource configuration. + * <p/> + * This implementation defers to the + * {@link org.glassfish.jersey.server.ContainerFactory#createContainer(Class, javax.ws.rs.core.Application)} method + * for creating an Container that manages the root resources. + * + * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be + * used as context path, the rest will be ignored. + * @param configuration web application configuration. + * @param start if set to false, server will not get started, which allows to configure the underlying + * transport layer, see above for details. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + */ + public static Server createServer(final URI uri, final ResourceConfig configuration, final boolean start) + throws ProcessingException { + return createServer(uri, null, ContainerFactory.createContainer(JettyHttpContainer.class, configuration), start); + } + + + /** + * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that + * in turn manages all root resource and provider classes declared by the + * resource configuration. + * + * @param uri the URI to create the http server. The URI scheme must be + * equal to "https". The URI user information and host + * are ignored If the URI port is not present then port 143 will be + * used. The URI path, query and fragment components are ignored. + * @param config the resource configuration. + * @param parentContext DI provider specific context with application's registered bindings. + * @param start if set to false, server will not get started, this allows end users to set + * additional properties on the underlying listener. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + * @see JettyHttpContainer + * @since 2.12 + */ + public static Server createServer(final URI uri, final ResourceConfig config, final boolean start, + final Object parentContext) { + return createServer(uri, null, new JettyHttpContainer(config, parentContext), start); + } + + + /** + * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that + * in turn manages all root resource and provider classes declared by the + * resource configuration. + * + * @param uri the URI to create the http server. The URI scheme must be + * equal to "https". The URI user information and host + * are ignored If the URI port is not present then port 143 will be + * used. The URI path, query and fragment components are ignored. + * @param config the resource configuration. + * @param parentContext DI provider specific context with application's registered bindings. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + * @see JettyHttpContainer + * @since 2.12 + */ + public static Server createServer(final URI uri, final ResourceConfig config, final Object parentContext) { + return createServer(uri, null, new JettyHttpContainer(config, parentContext), true); + } + + /** + * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that + * in turn manages all root resource and provider classes declared by the + * resource configuration. + * <p/> + * This implementation defers to the + * {@link ContainerFactory#createContainer(Class, javax.ws.rs.core.Application)} method + * for creating an Container that manages the root resources. + * + * @param uri the URI to create the http server. The URI scheme must be + * equal to {@code https}. The URI user information and host + * are ignored. If the URI port is not present then port + * {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be + * used. The URI path, query and fragment components are ignored. + * @param sslContextFactory this is the SSL context factory used to configure SSL connector + * @param config the resource configuration. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + */ + public static Server createServer(final URI uri, final SslContextFactory sslContextFactory, final ResourceConfig config) + throws ProcessingException { + final JettyHttpContainer container = ContainerFactory.createContainer(JettyHttpContainer.class, config); + return createServer(uri, sslContextFactory, container, true); + } + + /** + * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that + * in turn manages all root resource and provider classes found by searching the + * classes referenced in the java classpath. + * + * @param uri the URI to create the http server. The URI scheme must be + * equal to {@code https}. The URI user information and host + * are ignored. If the URI port is not present then port + * {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be + * used. The URI path, query and fragment components are ignored. + * @param sslContextFactory this is the SSL context factory used to configure SSL connector + * @param handler the container that handles all HTTP requests + * @param start if set to false, server will not get started, this allows end users to set + * additional properties on the underlying listener. + * @return newly created {@link Server}. + * + * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance. + * @throws IllegalArgumentException if {@code uri} is {@code null}. + * @see JettyHttpContainer + */ + public static Server createServer(final URI uri, + final SslContextFactory sslContextFactory, + final JettyHttpContainer handler, + final boolean start) { + if (uri == null) { + throw new IllegalArgumentException(LocalizationMessages.URI_CANNOT_BE_NULL()); + } + final String scheme = uri.getScheme(); + int defaultPort = Container.DEFAULT_HTTP_PORT; + + if (sslContextFactory == null) { + if (!"http".equalsIgnoreCase(scheme)) { + throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTP()); + } + } else { + if (!"https".equalsIgnoreCase(scheme)) { + throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTPS()); + } + defaultPort = Container.DEFAULT_HTTPS_PORT; + } + final int port = (uri.getPort() == -1) ? defaultPort : uri.getPort(); + + final Server server = new Server(new JettyConnectorThreadPool()); + final HttpConfiguration config = new HttpConfiguration(); + if (sslContextFactory != null) { + config.setSecureScheme("https"); + config.setSecurePort(port); + config.addCustomizer(new SecureRequestCustomizer()); + + final ServerConnector https = new ServerConnector(server, + new SslConnectionFactory(sslContextFactory, "http/1.1"), + new HttpConnectionFactory(config)); + https.setPort(port); + server.setConnectors(new Connector[]{https}); + + } else { + final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(config)); + http.setPort(port); + server.setConnectors(new Connector[]{http}); + } + if (handler != null) { + server.setHandler(handler); + } + + if (start) { + try { + // Start the server. + server.start(); + } catch (final Exception e) { + throw new ProcessingException(LocalizationMessages.ERROR_WHEN_CREATING_SERVER(), e); + } + } + return server; + } + + private static final class JettyConnectorThreadPool extends QueuedThreadPool { + private final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("jetty-http-server-%d") + .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler()) + .build(); + + @Override + protected Thread newThread(Runnable runnable) { + return threadFactory.newThread(runnable); + } + } +}
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java new file mode 100644 index 0000000..15afcbf --- /dev/null +++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java
@@ -0,0 +1,42 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.server.spi.ContainerProvider; + +import org.eclipse.jetty.server.Handler; + +/** + * Container provider for containers based on Jetty Server {@link org.eclipse.jetty.server.Handler}. + * + * @author Arul Dhesiaseelan (aruld@acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class JettyHttpContainerProvider implements ContainerProvider { + + @Override + public <T> T createContainer(final Class<T> type, final Application application) throws ProcessingException { + if (Handler.class == type || JettyHttpContainer.class == type) { + return type.cast(new JettyHttpContainer(application)); + } + return null; + } + +}
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/package-info.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/package-info.java new file mode 100644 index 0000000..4ea14bf --- /dev/null +++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Jetty container classes. + */ +package org.glassfish.jersey.jetty;
diff --git a/containers/jetty-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/jetty-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000..1496edb --- /dev/null +++ b/containers/jetty-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.jetty.JettyHttpContainerProvider \ No newline at end of file
diff --git a/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties b/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties new file mode 100644 index 0000000..b1528f8 --- /dev/null +++ b/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties
@@ -0,0 +1,23 @@ +# +# Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +# {0} - status code; {1} - status reason message +exception.sending.error.response=I/O exception occurred while sending "{0}/{1}" error response. +error.when.creating.server=Exception thrown when trying to create jetty server. +unable.to.close.response=Unable to close response output. +uri.cannot.be.null=The URI must not be null. +wrong.scheme.when.using.http=The URI scheme should be 'http' when not using SSL. +wrong.scheme.when.using.https=The URI scheme should be 'https' when using SSL.
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java new file mode 100644 index 0000000..dcfe4b7 --- /dev/null +++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java
@@ -0,0 +1,114 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import java.net.URI; +import java.security.AccessController; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.server.ResourceConfig; + +import org.eclipse.jetty.server.Server; +import org.junit.After; + +/** + * Abstract Jetty Server unit tester. + * + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Miroslav Fuksa + */ +public abstract class AbstractJettyServerTester { + + private static final Logger LOGGER = Logger.getLogger(AbstractJettyServerTester.class.getName()); + + public static final String CONTEXT = ""; + private static final int DEFAULT_PORT = 9998; + + /** + * Get the port to be used for test application deployments. + * + * @return The HTTP port of the URI + */ + protected final int getPort() { + final String value = AccessController + .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port")); + if (value != null) { + + try { + final int i = Integer.parseInt(value); + if (i <= 0) { + throw new NumberFormatException("Value not positive."); + } + return i; + } catch (NumberFormatException e) { + LOGGER.log(Level.CONFIG, + "Value of 'jersey.config.test.container.port'" + + " property is not a valid positive integer [" + value + "]." + + " Reverting to default [" + DEFAULT_PORT + "].", + e); + } + } + return DEFAULT_PORT; + } + + private volatile Server server; + + public UriBuilder getUri() { + return UriBuilder.fromUri("http://localhost").port(getPort()).path(CONTEXT); + } + + public void startServer(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final URI baseUri = getBaseUri(); + server = JettyHttpContainerFactory.createServer(baseUri, config); + LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + baseUri); + } + + public void startServer(ResourceConfig config) { + final URI baseUri = getBaseUri(); + server = JettyHttpContainerFactory.createServer(baseUri, config); + LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + baseUri); + } + + public URI getBaseUri() { + return UriBuilder.fromUri("http://localhost/").port(getPort()).build(); + } + + public void stopServer() { + try { + server.stop(); + server = null; + LOGGER.log(Level.INFO, "Jetty-http server stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @After + public void tearDown() { + if (server != null) { + stopServer(); + } + } +}
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java new file mode 100644 index 0000000..b4d6b81 --- /dev/null +++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java
@@ -0,0 +1,169 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.container.TimeoutHandler; +import javax.ws.rs.core.Response; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Michal Gajdos + */ +public class AsyncTest extends AbstractJettyServerTester { + + @Path("/async") + @SuppressWarnings("VoidMethodAnnotatedWithGET") + public static class AsyncResource { + + public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0); + + @GET + public void asyncGet(@Suspended final AsyncResponse asyncResponse) { + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep() + try { + Thread.sleep(5000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(final AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.") + .build()); + } + }); + asyncResponse.setTimeout(3, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep() + try { + Thread.sleep(7000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("multiple-invocations") + public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) { + INVOCATION_COUNT.incrementAndGet(); + + new Thread(new Runnable() { + @Override + public void run() { + asyncResponse.resume("OK"); + } + }).start(); + } + } + + private Client client; + + @Before + public void setUp() throws Exception { + startServer(AsyncResource.class); + client = ClientBuilder.newClient(); + } + + @Override + @After + public void tearDown() { + super.tearDown(); + client = null; + } + + @Test + public void testAsyncGet() throws ExecutionException, InterruptedException { + final Future<Response> responseFuture = client.target(getUri().path("/async")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + // get() waits for the response + assertEquals("DONE", response.readEntity(String.class)); + } + + @Test + public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException { + final Future<Response> responseFuture = client.target(getUri().path("/async/timeout")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } + + /** + * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request. + */ + @Test + public void testAsyncMultipleInvocations() throws Exception { + final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get(); + + assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1)); + + assertThat(response.getStatus(), is(200)); + assertThat(response.readEntity(String.class), is("OK")); + } +}
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java new file mode 100644 index 0000000..e934e6e --- /dev/null +++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java
@@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +/** + * @author Paul Sandoz + */ +public class ExceptionTest extends AbstractJettyServerTester { + @Path("{status}") + public static class ExceptionResource { + @GET + public String get(@PathParam("status") int status) { + throw new WebApplicationException(status); + } + + } + + @Test + public void test400StatusCode() throws IOException { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("400").build()); + assertEquals(400, r.request().get(Response.class).getStatus()); + } + + @Test + public void test500StatusCode() { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("500").build()); + + assertEquals(500, r.request().get(Response.class).getStatus()); + } +}
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java new file mode 100644 index 0000000..1377b88 --- /dev/null +++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java
@@ -0,0 +1,133 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener; +import org.glassfish.jersey.server.spi.Container; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + + +/** + * Reload and ContainerLifecycleListener support test. + * + * @author Paul Sandoz + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class LifecycleListenerTest extends AbstractJettyServerTester { + + @Path("/one") + public static class One { + @GET + public String get() { + return "one"; + } + } + + @Path("/two") + public static class Two { + @GET + public String get() { + return "two"; + } + } + + public static class Reloader extends AbstractContainerLifecycleListener { + Container container; + + public void reload(ResourceConfig newConfig) { + container.reload(newConfig); + } + + public void reload() { + container.reload(); + } + + @Override + public void onStartup(Container container) { + this.container = container; + } + + } + + @Test + public void testReload() { + final ResourceConfig rc = new ResourceConfig(One.class); + + Reloader reloader = new Reloader(); + rc.registerInstances(reloader); + + startServer(rc); + + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("/").build()); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals(404, r.path("two").request().get(Response.class).getStatus()); + + // add Two resource + reloader.reload(new ResourceConfig(One.class, Two.class)); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals("two", r.path("two").request().get(String.class)); + } + + static class StartStopListener extends AbstractContainerLifecycleListener { + volatile boolean started; + volatile boolean stopped; + + @Override + public void onStartup(Container container) { + started = true; + } + + @Override + public void onShutdown(Container container) { + stopped = true; + } + } + + @Test + public void testStartupShutdownHooks() { + final StartStopListener listener = new StartStopListener(); + + startServer(new ResourceConfig(One.class).register(listener)); + + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("/").build()); + + assertThat(r.path("one").request().get(String.class), equalTo("one")); + assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404)); + + stopServer(); + + assertTrue("ContainerLifecycleListener.onStartup has not been called.", listener.started); + assertTrue("ContainerLifecycleListener.onShutdown has not been called.", listener.stopped); + } +}
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java new file mode 100644 index 0000000..8866c33 --- /dev/null +++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty; + +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class OptionsTest extends AbstractJettyServerTester { + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + } + + @Test + public void testFooBarOptions() { + startServer(HelloWorldResource.class); + Client client = ClientBuilder.newClient(); + Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(0, response.getLength()); + assertEquals("foo/bar", response.getMediaType().toString()); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + +}
diff --git a/containers/jetty-servlet/pom.xml b/containers/jetty-servlet/pom.xml new file mode 100644 index 0000000..8ddf7b4 --- /dev/null +++ b/containers/jetty-servlet/pom.xml
@@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-jetty-servlet</artifactId> + <packaging>jar</packaging> + <name>jersey-container-jetty-servlet</name> + + <description>Jetty Servlet Container</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jetty-http</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-webapp</artifactId> + </dependency> + </dependencies> + + <build> + <resources> + <resource> + <directory>${basedir}/src/main/java</directory> + <includes> + <include>META-INF/**/*</include> + </includes> + </resource> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + <extensions>true</extensions> + <configuration> + <instructions> + <Import-Package> + javax.servlet.*;version="[2.4,5.0)", + * + </Import-Package> + </instructions> + <unpackBundle>true</unpackBundle> + </configuration> + </plugin> + </plugins> + </build> +</project>
diff --git a/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java b/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java new file mode 100644 index 0000000..dd585ce --- /dev/null +++ b/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
@@ -0,0 +1,284 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jetty.servlet; + +import java.net.URI; +import java.util.Map; + +import javax.servlet.Servlet; + +import org.glassfish.jersey.jetty.JettyHttpContainerFactory; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.uri.UriComponent; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebXmlConfiguration; + +/** + * Factory for creating and starting Jetty {@link Server} instances + * for deploying a Servlet. + * <p/> + * The default deployed server is an instance of {@link ServletContainer}. + * <p/> + * If no initialization parameters are declared (or is null) then root + * resource and provider classes will be found by searching the classes + * referenced in the java classpath. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public final class JettyWebContainerFactory { + + private JettyWebContainerFactory() { + } + + /** + * Create a {@link Server} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(String u) + throws Exception { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u)); + } + + /** + * Create a {@link Server} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(String u, Map<String, String> initParams) + throws Exception { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u), initParams); + } + + /** + * Create a {@link Server} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(URI u) + throws Exception { + return create(u, ServletContainer.class); + } + + /** + * Create a {@link Server} that registers the {@link ServletContainer}. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(URI u, Map<String, String> initParams) + throws Exception { + return create(u, ServletContainer.class, initParams); + } + + /** + * Create a {@link Server} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(String u, Class<? extends Servlet> c) + throws Exception { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u), c); + } + + /** + * Create a {@link Server} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(String u, Class<? extends Servlet> c, + Map<String, String> initParams) + throws Exception { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + return create(URI.create(u), c, initParams); + } + + /** + * Create a {@link Server} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(URI u, Class<? extends Servlet> c) + throws Exception { + return create(u, c, null); + } + + /** + * Create a {@link Server} that registers the declared + * servlet class. + * + * @param u the URI to create the http server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param c the servlet class. + * @param initParams the servlet initialization parameters. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(URI u, Class<? extends Servlet> c, Map<String, String> initParams) + throws Exception { + return create(u, c, null, initParams, null); + } + + private static Server create(URI u, Class<? extends Servlet> c, Servlet servlet, + Map<String, String> initParams, Map<String, String> contextInitParams) + throws Exception { + if (u == null) { + throw new IllegalArgumentException("The URI must not be null"); + } + + String path = u.getPath(); + if (path == null) { + throw new IllegalArgumentException("The URI path, of the URI " + u + ", must be non-null"); + } else if (path.isEmpty()) { + throw new IllegalArgumentException("The URI path, of the URI " + u + ", must be present"); + } else if (path.charAt(0) != '/') { + throw new IllegalArgumentException("The URI path, of the URI " + u + ". must start with a '/'"); + } + + path = String.format("/%s", UriComponent.decodePath(u.getPath(), true).get(1).toString()); + WebAppContext context = new WebAppContext(); + context.setDisplayName("JettyContext"); + context.setContextPath(path); + context.setConfigurations(new Configuration[]{new WebXmlConfiguration()}); + ServletHolder holder; + if (c != null) { + holder = context.addServlet(c, "/*"); + } else { + holder = new ServletHolder(servlet); + context.addServlet(holder, "/*"); + } + + if (contextInitParams != null) { + for (Map.Entry<String, String> e : contextInitParams.entrySet()) { + context.setInitParameter(e.getKey(), e.getValue()); + } + } + + if (initParams != null) { + holder.setInitParameters(initParams); + } + + Server server = JettyHttpContainerFactory.createServer(u, false); + server.setHandler(context); + server.start(); + return server; + } + + /** + * Create a {@link Server} that registers the declared + * servlet instance. + * + * @param u the URI to create the HTTP server. The URI scheme must be + * equal to "http". The URI user information and host + * are ignored If the URI port is not present then port 80 will be + * used. The URI query and fragment components are ignored. Only first path segment will be used + * as context path, the rest will be ignored. + * @param servlet the servlet instance. + * @param initParams the servlet initialization parameters. + * @param contextInitParams the servlet context initialization parameters. + * @return the http server, with the endpoint started. + * @throws Exception if an error occurs creating the container. + * @throws IllegalArgumentException if HTTP server URI is {@code null}. + */ + public static Server create(URI u, Servlet servlet, Map<String, String> initParams, Map<String, String> contextInitParams) + throws Exception { + if (servlet == null) { + throw new IllegalArgumentException("The servlet must not be null"); + } + return create(u, null, servlet, initParams, contextInitParams); + } +}
diff --git a/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/package-info.java b/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/package-info.java new file mode 100644 index 0000000..0a643c3 --- /dev/null +++ b/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Jetty Servlet container classes. + */ +package org.glassfish.jersey.jetty.servlet;
diff --git a/containers/netty-http/pom.xml b/containers/netty-http/pom.xml new file mode 100644 index 0000000..beb0545 --- /dev/null +++ b/containers/netty-http/pom.xml
@@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-netty-http</artifactId> + <packaging>jar</packaging> + <name>jersey-container-netty-http</name> + + <description>Netty Http Container.</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.connectors</groupId> + <artifactId>jersey-netty-connector</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + </plugins> + </build> + +</project>
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java new file mode 100644 index 0000000..22660e2 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.net.URI; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http2.Http2Codec; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.stream.ChunkedWriteHandler; + +/** + * Choose the handler implementation based on Http protocol. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class HttpVersionChooser extends ApplicationProtocolNegotiationHandler { + + private final URI baseUri; + private final NettyHttpContainer container; + + HttpVersionChooser(URI baseUri, NettyHttpContainer container) { + super(ApplicationProtocolNames.HTTP_1_1); + + this.baseUri = baseUri; + this.container = container; + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(new Http2Codec(true, new JerseyHttp2ServerHandler(baseUri, container))); + return; + } + + if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { + ctx.pipeline().addLast(new HttpServerCodec(), + new ChunkedWriteHandler(), + new JerseyServerHandler(baseUri, container)); + return; + } + + throw new IllegalStateException("Unknown protocol: " + protocol); + } +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyHttp2ServerHandler.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyHttp2ServerHandler.java new file mode 100644 index 0000000..ddb1089 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyHttp2ServerHandler.java
@@ -0,0 +1,218 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +import javax.ws.rs.core.SecurityContext; + +import io.netty.buffer.ByteBufInputStream; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.netty.connector.internal.NettyInputStream; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.internal.ContainerUtils; + +/** + * Jersey Netty HTTP/2 handler. + * <p> + * Note that this implementation cannot be more experimental. Any contributions / feedback is welcomed. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +@ChannelHandler.Sharable +class JerseyHttp2ServerHandler extends ChannelDuplexHandler { + + private final URI baseUri; + private final LinkedBlockingDeque<InputStream> isList = new LinkedBlockingDeque<>(); + private final NettyHttpContainer container; + + /** + * Constructor. + * + * @param baseUri base {@link URI} of the container (includes context path, if any). + * @param container Netty container implementation. + */ + JerseyHttp2ServerHandler(URI baseUri, NettyHttpContainer container) { + this.baseUri = baseUri; + this.container = container; + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ctx.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2HeadersFrame) { + onHeadersRead(ctx, (Http2HeadersFrame) msg); + } else if (msg instanceof Http2DataFrame) { + onDataRead(ctx, (Http2DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } + } + + /** + * Process incoming data. + */ + private void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data) throws Exception { + isList.add(new ByteBufInputStream(data.content())); + if (data.isEndStream()) { + isList.add(NettyInputStream.END_OF_INPUT); + } + } + + /** + * Process incoming request (just a headers in this case, entity is processed separately). + */ + private void onHeadersRead(ChannelHandlerContext ctx, Http2HeadersFrame headers) throws Exception { + + final ContainerRequest requestContext = createContainerRequest(ctx, headers); + + requestContext.setWriter(new NettyHttp2ResponseWriter(ctx, headers, container)); + + // must be like this, since there is a blocking read from Jersey + container.getExecutorService().execute(new Runnable() { + @Override + public void run() { + container.getApplicationHandler().handle(requestContext); + } + }); + } + + /** + * Create Jersey {@link ContainerRequest} based on Netty {@link HttpRequest}. + * + * @param ctx Netty channel context. + * @param http2Headers Netty Http/2 headers. + * @return created Jersey Container Request. + */ + private ContainerRequest createContainerRequest(ChannelHandlerContext ctx, Http2HeadersFrame http2Headers) { + + String path = http2Headers.headers().path().toString(); + + String s = path.startsWith("/") ? path.substring(1) : path; + URI requestUri = URI.create(baseUri + ContainerUtils.encodeUnsafeCharacters(s)); + + ContainerRequest requestContext = new ContainerRequest( + baseUri, requestUri, http2Headers.headers().method().toString(), getSecurityContext(), + new PropertiesDelegate() { + + private final Map<String, Object> properties = new HashMap<>(); + + @Override + public Object getProperty(String name) { + return properties.get(name); + } + + @Override + public Collection<String> getPropertyNames() { + return properties.keySet(); + } + + @Override + public void setProperty(String name, Object object) { + properties.put(name, object); + } + + @Override + public void removeProperty(String name) { + properties.remove(name); + } + }); + + // request entity handling. + if (!http2Headers.isEndStream()) { + + ctx.channel().closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + isList.add(NettyInputStream.END_OF_INPUT_ERROR); + } + }); + + requestContext.setEntityStream(new NettyInputStream(isList)); + } else { + requestContext.setEntityStream(new InputStream() { + @Override + public int read() throws IOException { + return -1; + } + }); + } + + // copying headers from netty request to jersey container request context. + for (CharSequence name : http2Headers.headers().names()) { + requestContext.headers(name.toString(), mapToString(http2Headers.headers().getAll(name))); + } + + return requestContext; + } + + private List<String> mapToString(List<CharSequence> list) { + ArrayList<String> result = new ArrayList<>(list.size()); + + for (CharSequence sequence : list) { + result.add(sequence.toString()); + } + + return result; + } + + private SecurityContext getSecurityContext() { + return new SecurityContext() { + + @Override + public boolean isUserInRole(final String role) { + return false; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getAuthenticationScheme() { + return null; + } + }; + } +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java new file mode 100644 index 0000000..33437e2 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java
@@ -0,0 +1,209 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.Principal; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +import javax.ws.rs.core.SecurityContext; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.netty.connector.internal.NettyInputStream; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.internal.ContainerUtils; + +/** + * {@link io.netty.channel.ChannelInboundHandler} which servers as a bridge + * between Netty and Jersey. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class JerseyServerHandler extends ChannelInboundHandlerAdapter { + + private final URI baseUri; + private final LinkedBlockingDeque<InputStream> isList = new LinkedBlockingDeque<>(); + private final NettyHttpContainer container; + + /** + * Constructor. + * + * @param baseUri base {@link URI} of the container (includes context path, if any). + * @param container Netty container implementation. + */ + public JerseyServerHandler(URI baseUri, NettyHttpContainer container) { + this.baseUri = baseUri; + this.container = container; + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + + if (msg instanceof HttpRequest) { + final HttpRequest req = (HttpRequest) msg; + + if (HttpUtil.is100ContinueExpected(req)) { + ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); + } + + isList.clear(); // clearing the content - possible leftover from previous request processing. + final ContainerRequest requestContext = createContainerRequest(ctx, req); + + requestContext.setWriter(new NettyResponseWriter(ctx, req, container)); + + // must be like this, since there is a blocking read from Jersey + container.getExecutorService().execute(new Runnable() { + @Override + public void run() { + container.getApplicationHandler().handle(requestContext); + } + }); + } + + if (msg instanceof HttpContent) { + HttpContent httpContent = (HttpContent) msg; + + ByteBuf content = httpContent.content(); + + if (content.isReadable()) { + isList.add(new ByteBufInputStream(content)); + } + + if (msg instanceof LastHttpContent) { + isList.add(NettyInputStream.END_OF_INPUT); + } + } + } + + /** + * Create Jersey {@link ContainerRequest} based on Netty {@link HttpRequest}. + * + * @param ctx Netty channel context. + * @param req Netty Http request. + * @return created Jersey Container Request. + */ + private ContainerRequest createContainerRequest(ChannelHandlerContext ctx, HttpRequest req) { + + String s = req.uri().startsWith("/") ? req.uri().substring(1) : req.uri(); + URI requestUri = URI.create(baseUri + ContainerUtils.encodeUnsafeCharacters(s)); + + ContainerRequest requestContext = new ContainerRequest( + baseUri, requestUri, req.method().name(), getSecurityContext(), + new PropertiesDelegate() { + + private final Map<String, Object> properties = new HashMap<>(); + + @Override + public Object getProperty(String name) { + return properties.get(name); + } + + @Override + public Collection<String> getPropertyNames() { + return properties.keySet(); + } + + @Override + public void setProperty(String name, Object object) { + properties.put(name, object); + } + + @Override + public void removeProperty(String name) { + properties.remove(name); + } + }); + + // request entity handling. + if ((req.headers().contains(HttpHeaderNames.CONTENT_LENGTH) && HttpUtil.getContentLength(req) > 0) + || HttpUtil.isTransferEncodingChunked(req)) { + + ctx.channel().closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + isList.add(NettyInputStream.END_OF_INPUT_ERROR); + } + }); + + requestContext.setEntityStream(new NettyInputStream(isList)); + } else { + requestContext.setEntityStream(new InputStream() { + @Override + public int read() throws IOException { + return -1; + } + }); + } + + // copying headers from netty request to jersey container request context. + for (String name : req.headers().names()) { + requestContext.headers(name, req.headers().getAll(name)); + } + + return requestContext; + } + + private SecurityContext getSecurityContext() { + return new SecurityContext() { + + @Override + public boolean isUserInRole(final String role) { + return false; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getAuthenticationScheme() { + return null; + } + }; + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ctx.close(); + } + +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java new file mode 100644 index 0000000..16a6ce0 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java
@@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.net.URI; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http2.Http2Codec; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.util.AsciiString; + +/** + * Jersey {@link ChannelInitializer}. + * <p> + * Adds {@link HttpServerCodec}, {@link ChunkedWriteHandler} and {@link JerseyServerHandler} to the channels pipeline. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class JerseyServerInitializer extends ChannelInitializer<SocketChannel> { + + private final URI baseUri; + private final SslContext sslCtx; + private final NettyHttpContainer container; + private final boolean http2; + + /** + * Constructor. + * + * @param baseUri base {@link URI} of the container (includes context path, if any). + * @param sslCtx SSL context. + * @param container Netty container implementation. + */ + public JerseyServerInitializer(URI baseUri, SslContext sslCtx, NettyHttpContainer container) { + this(baseUri, sslCtx, container, false); + } + + /** + * Constructor. + * + * @param baseUri base {@link URI} of the container (includes context path, if any). + * @param sslCtx SSL context. + * @param container Netty container implementation. + * @param http2 Http/2 protocol support. + */ + public JerseyServerInitializer(URI baseUri, SslContext sslCtx, NettyHttpContainer container, boolean http2) { + this.baseUri = baseUri; + this.sslCtx = sslCtx; + this.container = container; + this.http2 = http2; + } + + @Override + public void initChannel(SocketChannel ch) { + if (http2) { + + if (sslCtx != null) { + configureSsl(ch); + } else { + configureClearText(ch); + } + + } else { + ChannelPipeline p = ch.pipeline(); + if (sslCtx != null) { + p.addLast(sslCtx.newHandler(ch.alloc())); + } + p.addLast(new HttpServerCodec()); + p.addLast(new ChunkedWriteHandler()); + p.addLast(new JerseyServerHandler(baseUri, container)); + } + } + + /** + * Configure the pipeline for TLS NPN negotiation to HTTP/2. + */ + private void configureSsl(SocketChannel ch) { + ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()), new HttpVersionChooser(baseUri, container)); + } + + /** + * Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2. + */ + private void configureClearText(SocketChannel ch) { + final ChannelPipeline p = ch.pipeline(); + final HttpServerCodec sourceCodec = new HttpServerCodec(); + + p.addLast(sourceCodec); + p.addLast(new HttpServerUpgradeHandler(sourceCodec, new HttpServerUpgradeHandler.UpgradeCodecFactory() { + @Override + public HttpServerUpgradeHandler.UpgradeCodec newUpgradeCodec(CharSequence protocol) { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec(new Http2Codec(true, new JerseyHttp2ServerHandler(baseUri, container))); + } else { + return null; + } + } + })); + p.addLast(new SimpleChannelInboundHandler<HttpMessage>() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { + // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. + // "Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); + + ChannelPipeline pipeline = ctx.pipeline(); + ChannelHandlerContext thisCtx = pipeline.context(this); + pipeline.addAfter(thisCtx.name(), null, new JerseyServerHandler(baseUri, container)); + pipeline.replace(this, null, new ChunkedWriteHandler()); + ctx.fireChannelRead(msg); + } + }); + } +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttp2ResponseWriter.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttp2ResponseWriter.java new file mode 100644 index 0000000..327d894 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttp2ResponseWriter.java
@@ -0,0 +1,176 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; + +/** + * Netty implementation of {@link ContainerResponseWriter}. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class NettyHttp2ResponseWriter implements ContainerResponseWriter { + + private final ChannelHandlerContext ctx; + private final Http2HeadersFrame headersFrame; + private final NettyHttpContainer container; + + private volatile ScheduledFuture<?> suspendTimeoutFuture; + private volatile Runnable suspendTimeoutHandler; + + NettyHttp2ResponseWriter(ChannelHandlerContext ctx, Http2HeadersFrame headersFrame, NettyHttpContainer container) { + this.ctx = ctx; + this.headersFrame = headersFrame; + this.container = container; + } + + @Override + public OutputStream writeResponseStatusAndHeaders(long contentLength, ContainerResponse responseContext) + throws ContainerException { + + String reasonPhrase = responseContext.getStatusInfo().getReasonPhrase(); + int statusCode = responseContext.getStatus(); + + HttpResponseStatus status = reasonPhrase == null + ? HttpResponseStatus.valueOf(statusCode) + : new HttpResponseStatus(statusCode, reasonPhrase); + + DefaultHttp2Headers response = new DefaultHttp2Headers(); + response.status(Integer.toString(responseContext.getStatus())); + + for (final Map.Entry<String, List<String>> e : responseContext.getStringHeaders().entrySet()) { + response.add(e.getKey().toLowerCase(), e.getValue()); + } + + response.set(HttpHeaderNames.CONTENT_LENGTH, Long.toString(contentLength)); + + ctx.writeAndFlush(new DefaultHttp2HeadersFrame(response)); + + if (!headersFrame.headers().method().equals(HttpMethod.HEAD.asciiName()) + && (contentLength > 0 || contentLength == -1)) { + + return new OutputStream() { + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + + ByteBuf buffer = ctx.alloc().buffer(len); + buffer.writeBytes(b, off, len); + + ctx.writeAndFlush(new DefaultHttp2DataFrame(buffer, false)); + } + + @Override + public void flush() throws IOException { + ctx.flush(); + } + + @Override + public void close() throws IOException { + ctx.write(new DefaultHttp2DataFrame(true)).addListener(NettyResponseWriter.FLUSH_FUTURE); + } + }; + + } else { + ctx.writeAndFlush(new DefaultHttp2DataFrame(true)); + return null; + } + } + + @Override + public boolean suspend(long timeOut, TimeUnit timeUnit, final ContainerResponseWriter.TimeoutHandler + timeoutHandler) { + + suspendTimeoutHandler = new Runnable() { + @Override + public void run() { + timeoutHandler.onTimeout(NettyHttp2ResponseWriter.this); + } + }; + + if (timeOut <= 0) { + return true; + } + + suspendTimeoutFuture = + container.getScheduledExecutorService().schedule(suspendTimeoutHandler, timeOut, timeUnit); + + return true; + } + + @Override + public void setSuspendTimeout(long timeOut, TimeUnit timeUnit) throws IllegalStateException { + + // suspend(0, .., ..) was called, so suspendTimeoutFuture is null. + if (suspendTimeoutFuture != null) { + suspendTimeoutFuture.cancel(true); + } + + if (timeOut <= 0) { + return; + } + + suspendTimeoutFuture = + container.getScheduledExecutorService().schedule(suspendTimeoutHandler, timeOut, timeUnit); + } + + @Override + public void commit() { + ctx.flush(); + } + + @Override + public void failure(Throwable error) { + ctx.writeAndFlush(new DefaultHttp2Headers().status(HttpResponseStatus.INTERNAL_SERVER_ERROR.codeAsText())) + .addListener(ChannelFutureListener.CLOSE); + } + + @Override + public boolean enableResponseBuffering() { + return true; + } +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpContainer.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpContainer.java new file mode 100644 index 0000000..c825a82 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpContainer.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.spi.ExecutorServiceProvider; +import org.glassfish.jersey.spi.ScheduledExecutorServiceProvider; + +/** + * Netty based implementation of a {@link Container}. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class NettyHttpContainer implements Container { + + private volatile ApplicationHandler appHandler; + + public NettyHttpContainer(Application application) { + this.appHandler = new ApplicationHandler(application); + this.appHandler.onStartup(this); + } + + @Override + public ResourceConfig getConfiguration() { + return appHandler.getConfiguration(); + } + + @Override + public ApplicationHandler getApplicationHandler() { + return appHandler; + } + + @Override + public void reload() { + reload(appHandler.getConfiguration()); + } + + @Override + public void reload(ResourceConfig configuration) { + appHandler.onShutdown(this); + + appHandler = new ApplicationHandler(configuration); + appHandler.onReload(this); + appHandler.onStartup(this); + } + + /** + * Get {@link java.util.concurrent.ExecutorService}. + * + * @return Executor service associated with this container. + */ + ExecutorService getExecutorService() { + return appHandler.getInjectionManager().getInstance(ExecutorServiceProvider.class).getExecutorService(); + } + + /** + * Get {@link ScheduledExecutorService}. + * + * @return Scheduled executor service associated with this container. + */ + ScheduledExecutorService getScheduledExecutorService() { + return appHandler.getInjectionManager().getInstance(ScheduledExecutorServiceProvider.class).getExecutorService(); + } +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpContainerProvider.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpContainerProvider.java new file mode 100644 index 0000000..124b36e --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpContainerProvider.java
@@ -0,0 +1,188 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.net.URI; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import org.glassfish.jersey.Beta; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.ContainerProvider; + +/** + * Netty implementation of {@link ContainerProvider}. + * <p> + * There is also a few "factory" methods for creating Netty server. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @since 2.24 + */ +@Beta +public class NettyHttpContainerProvider implements ContainerProvider { + + @Override + public <T> T createContainer(Class<T> type, Application application) throws ProcessingException { + if (NettyHttpContainer.class == type) { + return type.cast(new NettyHttpContainer(application)); + } + + return null; + } + + /** + * Create and start Netty server. + * + * @param baseUri base uri. + * @param configuration Jersey configuration. + * @param sslContext Netty SSL context (can be null). + * @param block when {@code true}, this method will block until the server is stopped. When {@code false}, the + * execution will + * end immediately after the server is started. + * @return Netty channel instance. + * @throws ProcessingException when there is an issue with creating new container. + */ + public static Channel createServer(final URI baseUri, final ResourceConfig configuration, SslContext sslContext, + final boolean block) + throws ProcessingException { + + // Configure the server. + final EventLoopGroup bossGroup = new NioEventLoopGroup(1); + final EventLoopGroup workerGroup = new NioEventLoopGroup(); + final NettyHttpContainer container = new NettyHttpContainer(configuration); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new JerseyServerInitializer(baseUri, sslContext, container)); + + int port = getPort(baseUri); + + Channel ch = b.bind(port).sync().channel(); + + ch.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + container.getApplicationHandler().onShutdown(container); + + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + }); + + if (block) { + ch.closeFuture().sync(); + return ch; + } else { + return ch; + } + } catch (InterruptedException e) { + throw new ProcessingException(e); + } + } + + /** + * Create and start Netty server. + * + * @param baseUri base uri. + * @param configuration Jersey configuration. + * @param block when {@code true}, this method will block until the server is stopped. When {@code false}, the + * execution will + * end immediately after the server is started. + * @return Netty channel instance. + * @throws ProcessingException when there is an issue with creating new container. + */ + public static Channel createServer(final URI baseUri, final ResourceConfig configuration, final boolean block) + throws ProcessingException { + + return createServer(baseUri, configuration, null, block); + } + + /** + * Create and start Netty HTTP/2 server. + * <p> + * The server is capable of connection upgrade to HTTP/2. HTTP/1.x request will be server as they were used to. + * <p> + * Note that this implementation cannot be more experimental. Any contributions / feedback is welcomed. + * + * @param baseUri base uri. + * @param configuration Jersey configuration. + * @param sslContext Netty {@link SslContext}. + * @return Netty channel instance. + * @throws ProcessingException when there is an issue with creating new container. + */ + public static Channel createHttp2Server(final URI baseUri, final ResourceConfig configuration, SslContext sslContext) throws + ProcessingException { + + final EventLoopGroup bossGroup = new NioEventLoopGroup(1); + final EventLoopGroup workerGroup = new NioEventLoopGroup(); + final NettyHttpContainer container = new NettyHttpContainer(configuration); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new JerseyServerInitializer(baseUri, sslContext, container, true)); + + int port = getPort(baseUri); + + Channel ch = b.bind(port).sync().channel(); + + ch.closeFuture().addListener(new GenericFutureListener<Future<? super Void>>() { + @Override + public void operationComplete(Future<? super Void> future) throws Exception { + container.getApplicationHandler().onShutdown(container); + + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + }); + + return ch; + + } catch (InterruptedException e) { + throw new ProcessingException(e); + } + } + + private static int getPort(URI uri) { + if (uri.getPort() == -1) { + if ("http".equalsIgnoreCase(uri.getScheme())) { + return 80; + } else if ("https".equalsIgnoreCase(uri.getScheme())) { + return 443; + } + + throw new IllegalArgumentException("URI scheme must be 'http' or 'https'."); + } + + return uri.getPort(); + } +}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyResponseWriter.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyResponseWriter.java new file mode 100644 index 0000000..e50d870 --- /dev/null +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyResponseWriter.java
@@ -0,0 +1,187 @@ +/* + * Copyright (c) 2016, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.httpserver; + +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.glassfish.jersey.netty.connector.internal.JerseyChunkedInput; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpChunkedInput; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; + +/** + * Netty implementation of {@link ContainerResponseWriter}. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +class NettyResponseWriter implements ContainerResponseWriter { + + private static final Logger LOGGER = Logger.getLogger(NettyResponseWriter.class.getName()); + + static final ChannelFutureListener FLUSH_FUTURE = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + future.channel().flush(); + } + }; + + private final ChannelHandlerContext ctx; + private final HttpRequest req; + private final NettyHttpContainer container; + + private volatile ScheduledFuture<?> suspendTimeoutFuture; + private volatile Runnable suspendTimeoutHandler; + + private boolean responseWritten = false; + + NettyResponseWriter(ChannelHandlerContext ctx, HttpRequest req, NettyHttpContainer container) { + this.ctx = ctx; + this.req = req; + this.container = container; + } + + @Override + public synchronized OutputStream writeResponseStatusAndHeaders(long contentLength, ContainerResponse responseContext) + throws ContainerException { + + if (responseWritten) { + LOGGER.log(Level.FINE, "Response already written."); + return null; + } + + responseWritten = true; + + String reasonPhrase = responseContext.getStatusInfo().getReasonPhrase(); + int statusCode = responseContext.getStatus(); + + HttpResponseStatus status = reasonPhrase == null + ? HttpResponseStatus.valueOf(statusCode) + : new HttpResponseStatus(statusCode, reasonPhrase); + + DefaultHttpResponse response; + if (contentLength == 0) { + response = new DefaultFullHttpResponse(req.protocolVersion(), status); + } else { + response = new DefaultHttpResponse(req.protocolVersion(), status); + } + + for (final Map.Entry<String, List<String>> e : responseContext.getStringHeaders().entrySet()) { + response.headers().add(e.getKey(), e.getValue()); + } + + if (contentLength == -1) { + HttpUtil.setTransferEncodingChunked(response, true); + } else { + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentLength); + } + + if (HttpUtil.isKeepAlive(req)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + + ctx.writeAndFlush(response); + + if (req.method() != HttpMethod.HEAD && (contentLength > 0 || contentLength == -1)) { + + JerseyChunkedInput jerseyChunkedInput = new JerseyChunkedInput(ctx.channel()); + + if (HttpUtil.isTransferEncodingChunked(response)) { + ctx.write(new HttpChunkedInput(jerseyChunkedInput)).addListener(FLUSH_FUTURE); + } else { + ctx.write(new HttpChunkedInput(jerseyChunkedInput)).addListener(FLUSH_FUTURE); + } + return jerseyChunkedInput; + + } else { + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + return null; + } + } + + @Override + public boolean suspend(long timeOut, TimeUnit timeUnit, final ContainerResponseWriter.TimeoutHandler + timeoutHandler) { + + suspendTimeoutHandler = new Runnable() { + @Override + public void run() { + timeoutHandler.onTimeout(NettyResponseWriter.this); + } + }; + + if (timeOut <= 0) { + return true; + } + + suspendTimeoutFuture = + container.getScheduledExecutorService().schedule(suspendTimeoutHandler, timeOut, timeUnit); + + return true; + } + + @Override + public void setSuspendTimeout(long timeOut, TimeUnit timeUnit) throws IllegalStateException { + + // suspend(0, .., ..) was called, so suspendTimeoutFuture is null. + if (suspendTimeoutFuture != null) { + suspendTimeoutFuture.cancel(true); + } + + if (timeOut <= 0) { + return; + } + + suspendTimeoutFuture = + container.getScheduledExecutorService().schedule(suspendTimeoutHandler, timeOut, timeUnit); + } + + @Override + public void commit() { + ctx.flush(); + } + + @Override + public void failure(Throwable error) { + ctx.writeAndFlush(new DefaultFullHttpResponse(req.protocolVersion(), HttpResponseStatus.INTERNAL_SERVER_ERROR)) + .addListener(ChannelFutureListener.CLOSE); + } + + @Override + public boolean enableResponseBuffering() { + return true; + } +}
diff --git a/containers/netty-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/netty-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000..96ee927 --- /dev/null +++ b/containers/netty-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.netty.httpserver.NettyHttpContainerProvider
diff --git a/containers/pom.xml b/containers/pom.xml new file mode 100644 index 0000000..2246a14 --- /dev/null +++ b/containers/pom.xml
@@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <packaging>pom</packaging> + <name>jersey-containers</name> + + <description>Jersey container providers umbrella project module</description> + + <modules> + <module>glassfish</module> + <module>grizzly2-http</module> + <module>grizzly2-servlet</module> + <module>jdk-http</module> + <module>jersey-servlet-core</module> + <module>jersey-servlet</module> + <module>jetty-http</module> + <module>jetty-servlet</module> + <module>netty-http</module> + <module>simple-http</module> + </modules> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project>
diff --git a/containers/simple-http/pom.xml b/containers/simple-http/pom.xml new file mode 100644 index 0000000..e3bfb38 --- /dev/null +++ b/containers/simple-http/pom.xml
@@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <artifactId>jersey-container-simple-http</artifactId> + <packaging>jar</packaging> + <name>jersey-container-simple-http</name> + + <description>Simple Http Container</description> + + <dependencies> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + <dependency> + <groupId>org.simpleframework</groupId> + <artifactId>simple-http</artifactId> + </dependency> + <dependency> + <groupId>org.simpleframework</groupId> + <artifactId>simple-transport</artifactId> + </dependency> + <dependency> + <groupId>org.simpleframework</groupId> + <artifactId>simple-common</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + </plugins> + + <resources> + <resource> + <directory>${basedir}/src/main/java</directory> + <includes> + <include>META-INF/**/*</include> + </includes> + </resource> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> + +</project>
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainer.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainer.java new file mode 100644 index 0000000..e59962f --- /dev/null +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainer.java
@@ -0,0 +1,468 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.SecurityContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.ExtendedLogger; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerException; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.internal.ContainerUtils; +import org.glassfish.jersey.server.spi.Container; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; +import org.glassfish.jersey.server.spi.ContainerResponseWriter.TimeoutHandler; + +import org.simpleframework.common.thread.DaemonFactory; +import org.simpleframework.http.Address; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.Status; + +/** + * Jersey {@code Container} implementation based on Simple framework + * {@link org.simpleframework.http.core.Container}. + * + * @author Arul Dhesiaseelan (aruld@acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class SimpleContainer implements org.simpleframework.http.core.Container, Container { + + private static final ExtendedLogger logger = + new ExtendedLogger(Logger.getLogger(SimpleContainer.class.getName()), Level.FINEST); + + private final Type RequestTYPE = (new GenericType<Ref<Request>>() { }).getType(); + private final Type ResponseTYPE = (new GenericType<Ref<Response>>() { }).getType(); + + /** + * Referencing factory for Simple request. + */ + private static class SimpleRequestReferencingFactory extends ReferencingFactory<Request> { + + @Inject + public SimpleRequestReferencingFactory(final Provider<Ref<Request>> referenceFactory) { + super(referenceFactory); + } + } + + /** + * Referencing factory for Simple response. + */ + private static class SimpleResponseReferencingFactory extends ReferencingFactory<Response> { + + @Inject + public SimpleResponseReferencingFactory(final Provider<Ref<Response>> referenceFactory) { + super(referenceFactory); + } + } + + /** + * An internal binder to enable Simple HTTP container specific types injection. This binder allows + * to inject underlying Grizzly HTTP request and response instances. + */ + private static class SimpleBinder extends AbstractBinder { + + @Override + protected void configure() { + bindFactory(SimpleRequestReferencingFactory.class).to(Request.class).proxy(true) + .proxyForSameScope(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.<Request>referenceFactory()) + .to(new GenericType<Ref<Request>>() { + }).in(RequestScoped.class); + + bindFactory(SimpleResponseReferencingFactory.class).to(Response.class).proxy(true) + .proxyForSameScope(false).in(RequestScoped.class); + bindFactory(ReferencingFactory.<Response>referenceFactory()) + .to(new GenericType<Ref<Response>>() { + }).in(RequestScoped.class); + } + } + + private volatile ScheduledExecutorService scheduler; + private volatile ApplicationHandler appHandler; + + private static final class ResponseWriter implements ContainerResponseWriter { + + private final AtomicReference<TimeoutTimer> reference; + private final ScheduledExecutorService scheduler; + private final Response response; + + ResponseWriter(final Response response, final ScheduledExecutorService scheduler) { + this.reference = new AtomicReference<TimeoutTimer>(); + this.response = response; + this.scheduler = scheduler; + } + + @Override + public OutputStream writeResponseStatusAndHeaders(final long contentLength, + final ContainerResponse context) throws ContainerException { + final javax.ws.rs.core.Response.StatusType statusInfo = context.getStatusInfo(); + + final int code = statusInfo.getStatusCode(); + final String reason = statusInfo.getReasonPhrase() == null + ? Status.getDescription(code) + : statusInfo.getReasonPhrase(); + response.setCode(code); + response.setDescription(reason); + + if (contentLength != -1) { + response.setContentLength(contentLength); + } + for (final Map.Entry<String, List<String>> e : context.getStringHeaders().entrySet()) { + for (final String value : e.getValue()) { + response.addValue(e.getKey(), value); + } + } + + try { + return response.getOutputStream(); + } catch (final IOException ioe) { + throw new ContainerException("Error during writing out the response headers.", ioe); + } + } + + @Override + public boolean suspend(final long timeOut, final TimeUnit timeUnit, + final TimeoutHandler timeoutHandler) { + try { + TimeoutTimer timer = reference.get(); + + if (timer == null) { + TimeoutDispatcher task = new TimeoutDispatcher(this, timeoutHandler); + ScheduledFuture<?> future = + scheduler.schedule(task, timeOut == 0 ? Integer.MAX_VALUE : timeOut, + timeOut == 0 ? TimeUnit.SECONDS : timeUnit); + timer = new TimeoutTimer(scheduler, future, task); + reference.set(timer); + return true; + } + return false; + } catch (final IllegalStateException ex) { + return false; + } finally { + logger.debugLog("suspend(...) called"); + } + } + + @Override + public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) + throws IllegalStateException { + try { + TimeoutTimer timer = reference.get(); + + if (timer == null) { + throw new IllegalStateException("Response has not been suspended"); + } + timer.reschedule(timeOut, timeUnit); + } finally { + logger.debugLog("setTimeout(...) called"); + } + } + + @Override + public void commit() { + try { + response.close(); + } catch (final IOException e) { + logger.log(Level.SEVERE, "Unable to send 500 error response.", e); + } finally { + logger.debugLog("commit() called"); + } + } + + public boolean isSuspended() { + return reference.get() != null; + } + + @Override + public void failure(final Throwable error) { + try { + if (!response.isCommitted()) { + response.setCode(javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + response.setDescription(error.getMessage()); + } + } finally { + logger.debugLog("failure(...) called"); + commit(); + rethrow(error); + } + + } + + @Override + public boolean enableResponseBuffering() { + return false; + } + + /** + * Rethrow the original exception as required by JAX-RS, 3.3.4 + * + * @param error throwable to be re-thrown + */ + private void rethrow(final Throwable error) { + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new ContainerException(error); + } + } + + } + + private static final class TimeoutTimer { + + private final AtomicReference<ScheduledFuture<?>> reference; + private final ScheduledExecutorService service; + private final TimeoutDispatcher task; + + public TimeoutTimer(ScheduledExecutorService service, ScheduledFuture<?> future, + TimeoutDispatcher task) { + this.reference = new AtomicReference<ScheduledFuture<?>>(); + this.service = service; + this.task = task; + } + + public void reschedule(long timeOut, TimeUnit timeUnit) { + ScheduledFuture<?> future = reference.getAndSet(null); + + if (future != null) { + if (future.cancel(false)) { + future = service.schedule(task, timeOut == 0 ? Integer.MAX_VALUE : timeOut, + timeOut == 0 ? TimeUnit.SECONDS : timeUnit); + reference.set(future); + } + } else { + future = service.schedule(task, timeOut == 0 ? Integer.MAX_VALUE : timeOut, + timeOut == 0 ? TimeUnit.SECONDS : timeUnit); + reference.set(future); + } + } + } + + private static final class TimeoutDispatcher implements Runnable { + + private final ResponseWriter writer; + private final TimeoutHandler handler; + + public TimeoutDispatcher(ResponseWriter writer, TimeoutHandler handler) { + this.writer = writer; + this.handler = handler; + } + + public void run() { + try { + handler.onTimeout(writer); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to call timeout handler", e); + } + } + } + + @Override + public void handle(final Request request, final Response response) { + final ResponseWriter responseWriter = new ResponseWriter(response, scheduler); + final URI baseUri = getBaseUri(request); + final URI requestUri = getRequestUri(request, baseUri); + + try { + final ContainerRequest requestContext = new ContainerRequest(baseUri, requestUri, + request.getMethod(), getSecurityContext(request), new MapPropertiesDelegate()); + requestContext.setEntityStream(request.getInputStream()); + for (final String headerName : request.getNames()) { + requestContext.headers(headerName, request.getValue(headerName)); + } + requestContext.setWriter(responseWriter); + requestContext.setRequestScopedInitializer(injectionManager -> { + injectionManager.<Ref<Request>>getInstance(RequestTYPE).set(request); + injectionManager.<Ref<Response>>getInstance(ResponseTYPE).set(response); + }); + + appHandler.handle(requestContext); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } finally { + if (!responseWriter.isSuspended()) { + close(response); + } + } + } + + private URI getRequestUri(final Request request, final URI baseUri) { + try { + final String serverAddress = getServerAddress(baseUri); + String uri = ContainerUtils.getHandlerPath(request.getTarget()); + + final String queryString = request.getQuery().toString(); + if (queryString != null) { + uri = uri + "?" + ContainerUtils.encodeUnsafeCharacters(queryString); + } + + return new URI(serverAddress + uri); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private String getServerAddress(final URI baseUri) throws URISyntaxException { + return new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), null, null, + null).toString(); + } + + private URI getBaseUri(final Request request) { + try { + final String hostHeader = request.getValue("Host"); + + if (hostHeader != null) { + final String scheme = request.isSecure() ? "https" : "http"; + return new URI(scheme + "://" + hostHeader + "/"); + } else { + final Address address = request.getAddress(); + return new URI(address.getScheme(), null, address.getDomain(), address.getPort(), "/", null, + null); + } + } catch (final URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + + private SecurityContext getSecurityContext(final Request request) { + return new SecurityContext() { + + @Override + public boolean isUserInRole(final String role) { + return false; + } + + @Override + public boolean isSecure() { + return request.isSecure(); + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getAuthenticationScheme() { + return null; + } + }; + } + + private void close(final Response response) { + try { + response.close(); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + } + + @Override + public ResourceConfig getConfiguration() { + return appHandler.getConfiguration(); + } + + @Override + public void reload() { + reload(getConfiguration()); + } + + @Override + public void reload(final ResourceConfig configuration) { + appHandler.onShutdown(this); + + appHandler = new ApplicationHandler(configuration.register(new SimpleBinder())); + scheduler = new ScheduledThreadPoolExecutor(2, new DaemonFactory(TimeoutDispatcher.class)); + appHandler.onReload(this); + appHandler.onStartup(this); + } + + @Override + public ApplicationHandler getApplicationHandler() { + return appHandler; + } + + /** + * Inform this container that the server has been started. + * <p/> + * This method must be implicitly called after the server containing this container is started. + */ + void onServerStart() { + appHandler.onStartup(this); + } + + /** + * Inform this container that the server is being stopped. + * <p/> + * This method must be implicitly called before the server containing this container is stopped. + */ + void onServerStop() { + appHandler.onShutdown(this); + scheduler.shutdown(); + } + + /** + * Create a new Simple framework HTTP container. + * + * @param application JAX-RS / Jersey application to be deployed on Simple framework HTTP container. + * @param parentContext DI provider specific context with application's registered bindings. + */ + SimpleContainer(final Application application, final Object parentContext) { + this.appHandler = new ApplicationHandler(application, new SimpleBinder(), parentContext); + this.scheduler = new ScheduledThreadPoolExecutor(2, new DaemonFactory(TimeoutDispatcher.class)); + } + + /** + * Create a new Simple framework HTTP container. + * + * @param application JAX-RS / Jersey application to be deployed on Simple framework HTTP + * container. + */ + SimpleContainer(final Application application) { + this.appHandler = new ApplicationHandler(application, new SimpleBinder()); + this.scheduler = new ScheduledThreadPoolExecutor(2, new DaemonFactory(TimeoutDispatcher.class)); + } +}
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainerFactory.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainerFactory.java new file mode 100644 index 0000000..a9dc46d --- /dev/null +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainerFactory.java
@@ -0,0 +1,275 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; + +import javax.ws.rs.ProcessingException; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.internal.util.collection.UnsafeValue; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.simple.internal.LocalizationMessages; + +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerSocketProcessor; +import org.simpleframework.transport.SocketProcessor; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; + +/** + * Factory for creating and starting Simple server containers. This returns a handle to the started + * server as {@link Closeable} instances, which allows the server to be stopped by invoking the + * {@link Closeable#close} method. + * <p/> + * To start the server in HTTPS mode an {@link SSLContext} can be provided. This will be used to + * decrypt and encrypt information sent over the connected TCP socket channel. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Paul Sandoz + */ +public final class SimpleContainerFactory { + + private SimpleContainerFactory() { + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes declared by the resource configuration. + * + * @param address the URI to create the http server. The URI scheme must be equal to "http". The + * URI user information and host are ignored If the URI port is not present then port 80 + * will be used. The URI path, query and fragment components are ignored. + * @param config the resource configuration. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + */ + public static SimpleServer create(final URI address, final ResourceConfig config) { + return create(address, null, new SimpleContainer(config)); + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes declared by the resource configuration. + * + * @param address the URI to create the http server. The URI scheme must be equal to "http". The + * URI user information and host are ignored If the URI port is not present then port 80 + * will be used. The URI path, query and fragment components are ignored. + * @param config the resource configuration. + * @param count this is the number of threads to be used. + * @param select this is the number of selector threads to use. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + */ + public static SimpleServer create(final URI address, final ResourceConfig config, final int count, + final int select) { + return create(address, null, new SimpleContainer(config), count, select); + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes declared by the resource configuration. + * + * @param address the URI to create the http server. The URI scheme must be equal to {@code https} + * . The URI user information and host are ignored. If the URI port is not present then + * port {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be used. + * The URI path, query and fragment components are ignored. + * @param context this is the SSL context used for SSL connections. + * @param config the resource configuration. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + */ + public static SimpleServer create(final URI address, final SSLContext context, + final ResourceConfig config) { + return create(address, context, new SimpleContainer(config)); + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes declared by the resource configuration. + * + * @param address the URI to create the http server. The URI scheme must be equal to {@code https} + * . The URI user information and host are ignored. If the URI port is not present then + * port {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be used. + * The URI path, query and fragment components are ignored. + * @param context this is the SSL context used for SSL connections. + * @param config the resource configuration. + * @param count this is the number of threads to be used. + * @param select this is the number of selector threads to use. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + */ + public static SimpleServer create(final URI address, final SSLContext context, + final ResourceConfig config, final int count, final int select) { + return create(address, context, new SimpleContainer(config), count, select); + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes found by searching the classes referenced in the java classpath. + * + * @param address the URI to create the http server. The URI scheme must be equal to {@code https} + * . The URI user information and host are ignored. If the URI port is not present then + * port {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be used. + * The URI path, query and fragment components are ignored. + * @param context this is the SSL context used for SSL connections. + * @param container the container that handles all HTTP requests. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + */ + public static SimpleServer create(final URI address, final SSLContext context, + final SimpleContainer container) { + return _create(address, context, container, new UnsafeValue<SocketProcessor, IOException>() { + @Override + public SocketProcessor get() throws IOException { + return new ContainerSocketProcessor(container); + } + }); + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes declared by the resource configuration. + * + * @param address the URI to create the http server. The URI scheme must be equal to {@code https} + * . The URI user information and host are ignored. If the URI port is not present then + * port {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be used. + * The URI path, query and fragment components are ignored. + * @param context this is the SSL context used for SSL connections. + * @param config the resource configuration. + * @param parentContext DI provider specific context with application's registered bindings. + * @param count this is the number of threads to be used. + * @param select this is the number of selector threads to use. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + * @since 2.12 + */ + public static SimpleServer create(final URI address, final SSLContext context, + final ResourceConfig config, final Object parentContext, final int count, + final int select) { + return create(address, context, new SimpleContainer(config, parentContext), count, select); + } + + /** + * Create a {@link Closeable} that registers an {@link Container} that in turn manages all root + * resource and provider classes found by searching the classes referenced in the java classpath. + * + * @param address the URI to create the http server. The URI scheme must be equal to {@code https} + * . The URI user information and host are ignored. If the URI port is not present then + * port {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be used. + * The URI path, query and fragment components are ignored. + * @param context this is the SSL context used for SSL connections. + * @param container the container that handles all HTTP requests. + * @param count this is the number of threads to be used. + * @param select this is the number of selector threads to use. + * @return the closeable connection, with the endpoint started. + * @throws ProcessingException thrown when problems during server creation. + * @throws IllegalArgumentException if {@code address} is {@code null}. + */ + public static SimpleServer create(final URI address, final SSLContext context, + final SimpleContainer container, final int count, final int select) + throws ProcessingException { + + return _create(address, context, container, new UnsafeValue<SocketProcessor, IOException>() { + @Override + public SocketProcessor get() throws IOException { + return new ContainerSocketProcessor(container, count, select); + } + }); + } + + private static SimpleServer _create(final URI address, final SSLContext context, + final SimpleContainer container, + final UnsafeValue<SocketProcessor, IOException> serverProvider) + throws ProcessingException { + if (address == null) { + throw new IllegalArgumentException(LocalizationMessages.URI_CANNOT_BE_NULL()); + } + final String scheme = address.getScheme(); + int defaultPort = org.glassfish.jersey.server.spi.Container.DEFAULT_HTTP_PORT; + + if (context == null) { + if (!scheme.equalsIgnoreCase("http")) { + throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTP()); + } + } else { + if (!scheme.equalsIgnoreCase("https")) { + throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTPS()); + } + defaultPort = org.glassfish.jersey.server.spi.Container.DEFAULT_HTTPS_PORT; + } + int port = address.getPort(); + + if (port == -1) { + port = defaultPort; + } + final InetSocketAddress listen = new InetSocketAddress(port); + final Connection connection; + try { + final SimpleTraceAnalyzer analyzer = new SimpleTraceAnalyzer(); + final SocketProcessor server = serverProvider.get(); + connection = new SocketConnection(server, analyzer); + + + final SocketAddress socketAddr = connection.connect(listen, context); + container.onServerStart(); + + return new SimpleServer() { + + @Override + public void close() throws IOException { + container.onServerStop(); + analyzer.stop(); + connection.close(); + } + + @Override + public int getPort() { + return ((InetSocketAddress) socketAddr).getPort(); + } + + @Override + public boolean isDebug() { + return analyzer.isActive(); + } + + @Override + public void setDebug(boolean enable) { + if (enable) { + analyzer.start(); + } else { + analyzer.stop(); + } + } + }; + } catch (final IOException ex) { + throw new ProcessingException(LocalizationMessages.ERROR_WHEN_CREATING_SERVER(), ex); + } + } +}
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainerProvider.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainerProvider.java new file mode 100644 index 0000000..68f8f0f --- /dev/null +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleContainerProvider.java
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.server.spi.ContainerProvider; + +import org.simpleframework.http.core.Container; + +/** + * Container provider for containers based on Simple HTTP Server + * {@link org.simpleframework.http.core.Container}. + * + * @author Marc Hadley + * @author Arul Dhesiaseelan (aruld@acm.org) + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class SimpleContainerProvider implements ContainerProvider { + + @Override + public <T> T createContainer(Class<T> type, Application application) throws ProcessingException { + if (Container.class == type || SimpleContainer.class == type) { + return type.cast(new SimpleContainer(application)); + } + return null; + } + +}
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleServer.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleServer.java new file mode 100644 index 0000000..02aec75 --- /dev/null +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleServer.java
@@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.io.Closeable; + +/** + * Simple server facade providing convenient methods to obtain info about the server (i.e. port). + * + * @author Michal Gajdos + * @since 2.9 + */ +public interface SimpleServer extends Closeable { + + /** + * The port the server is listening to for incomming HTTP connections. If the port is not + * specified the {@link org.glassfish.jersey.server.spi.Container.DEFAULT_PORT} is used. + * + * @return the port the server is listening on + */ + public int getPort(); + + /** + * If this is true then very low level I/O operations are logged. Typically this is used to debug + * I/O issues such as HTTPS handshakes or performance issues by analysing the various latencies + * involved in the HTTP conversation. + * <p/> + * There is a minimal performance penalty if this is enabled and it is perfectly suited to being + * enabled in a production environment, at the cost of logging overhead. + * + * @return {@code true} if debug is enabled, false otherwise. + * @since 2.23 + */ + public boolean isDebug(); + + /** + * To enable very low level logging this can be enabled. This goes far beyond logging issues such + * as connection establishment of request dispatch, it can trace the TCP operations latencies + * involved. + * + * @param enable if {@code true} debug tracing will be enabled. + * @since 2.23 + */ + public void setDebug(boolean enable); +}
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleTraceAnalyzer.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleTraceAnalyzer.java new file mode 100644 index 0000000..4873da1 --- /dev/null +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleTraceAnalyzer.java
@@ -0,0 +1,201 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.channels.SelectableChannel; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.glassfish.jersey.internal.util.ExtendedLogger; + +import org.simpleframework.common.thread.DaemonFactory; +import org.simpleframework.transport.trace.Trace; +import org.simpleframework.transport.trace.TraceAnalyzer; + +/** + * Tracing at a very low level can be performed with a {@link TraceAnalyzer}. This provides much + * more useful information than the conventional {@link LoggingFilter} in that it provides details + * at a very low level. This is very useful when monitoring performance interactions at the TCP + * level between clients and servers. + * <p/> + * Performance overhead for the server is minimal as events are pumped out in batches. The amount of + * logging information will increase quite significantly though. + * + * @author Niall Gallagher + */ +public class SimpleTraceAnalyzer implements TraceAnalyzer { + + private static final ExtendedLogger logger = + new ExtendedLogger(Logger.getLogger(SimpleTraceAnalyzer.class.getName()), Level.FINEST); + + private final TraceConsumer consumer; + private final ThreadFactory factory; + private final AtomicBoolean active; + private final AtomicLong count; + + /** + * Creates an asynchronous trace event logger. + */ + public SimpleTraceAnalyzer() { + this.factory = new DaemonFactory(TraceConsumer.class); + this.consumer = new TraceConsumer(); + this.active = new AtomicBoolean(); + this.count = new AtomicLong(); + } + + public boolean isActive() { + return active.get(); + } + + @Override + public Trace attach(SelectableChannel channel) { + long sequence = count.getAndIncrement(); + return new TraceFeeder(channel, sequence); + } + + /** + * Begin logging trace events to the underlying logger. + */ + public void start() { + if (active.compareAndSet(false, true)) { + Thread thread = factory.newThread(consumer); + thread.start(); + } + } + + @Override + public void stop() { + active.set(false); + } + + private class TraceConsumer implements Runnable { + + private final Queue<TraceRecord> queue; + + public TraceConsumer() { + this.queue = new ConcurrentLinkedQueue<TraceRecord>(); + } + + public void consume(TraceRecord record) { + queue.offer(record); + } + + public void run() { + try { + while (active.get()) { + Thread.sleep(1000); + drain(); + } + } catch (Exception e) { + logger.info("Trace analyzer error"); + } finally { + try { + drain(); + } catch (Exception e) { + logger.info("Trace analyzer could not drain queue"); + } + active.set(false); + } + + } + + private void drain() { + while (!queue.isEmpty()) { + TraceRecord record = queue.poll(); + + if (record != null) { + String message = record.toString(); + logger.info(message); + } + } + } + } + + private class TraceFeeder implements Trace { + + private final SelectableChannel channel; + private final long sequence; + + public TraceFeeder(SelectableChannel channel, long sequence) { + this.sequence = sequence; + this.channel = channel; + } + + @Override + public void trace(Object event) { + trace(event, null); + } + + @Override + public void trace(Object event, Object value) { + if (active.get()) { + TraceRecord record = new TraceRecord(channel, event, value, sequence); + consumer.consume(record); + } + } + + } + + private class TraceRecord { + + private final SelectableChannel channel; + private final String thread; + private final Object event; + private final Object value; + private final long sequence; + + public TraceRecord(SelectableChannel channel, Object event, Object value, long sequence) { + this.thread = Thread.currentThread().getName(); + this.sequence = sequence; + this.channel = channel; + this.event = event; + this.value = value; + } + + public String toString() { + StringWriter builder = new StringWriter(); + PrintWriter writer = new PrintWriter(builder); + + writer.print(sequence); + writer.print(" "); + writer.print(channel); + writer.print(" ("); + writer.print(thread); + writer.print("): "); + writer.print(event); + + if (value != null) { + if (value instanceof Throwable) { + writer.print(" -> "); + ((Throwable) value).printStackTrace(writer); + } else { + writer.print(" -> "); + writer.print(value); + } + } + writer.close(); + return builder.toString(); + } + } +}
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/package-info.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/package-info.java new file mode 100644 index 0000000..f7a4d38 --- /dev/null +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey Simple 6.x container classes. + */ +package org.glassfish.jersey.simple;
diff --git a/containers/simple-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/simple-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider new file mode 100644 index 0000000..a0a4981 --- /dev/null +++ b/containers/simple-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@ +org.glassfish.jersey.simple.SimpleContainerProvider \ No newline at end of file
diff --git a/containers/simple-http/src/main/resources/org.glassfish.jersey.simple.internal/localization.properties b/containers/simple-http/src/main/resources/org.glassfish.jersey.simple.internal/localization.properties new file mode 100644 index 0000000..138fa5b --- /dev/null +++ b/containers/simple-http/src/main/resources/org.glassfish.jersey.simple.internal/localization.properties
@@ -0,0 +1,20 @@ +# +# Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +error.when.creating.server=IOException thrown when trying to create simple server. +uri.cannot.be.null=The URI must not be null. +wrong.scheme.when.using.http=The URI scheme should be 'http' when not using SSL. +wrong.scheme.when.using.https=The URI scheme should be 'https' when using SSL.
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/AbstractSimpleServerTester.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/AbstractSimpleServerTester.java new file mode 100644 index 0000000..eb653ca --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/AbstractSimpleServerTester.java
@@ -0,0 +1,136 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.net.URI; +import java.security.AccessController; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; + +import org.junit.After; + +/** + * Abstract Simple HTTP Server unit tester. + * + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Miroslav Fuksa + */ +public abstract class AbstractSimpleServerTester { + + public static final String CONTEXT = ""; + private final int DEFAULT_PORT = 9998; + + private static final Logger LOGGER = Logger.getLogger(AbstractSimpleServerTester.class.getName()); + + /** + * Get the port to be used for test application deployments. + * + * @return The HTTP port of the URI + */ + protected final int getPort() { + final String value = AccessController + .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port")); + if (value != null) { + + try { + final int i = Integer.parseInt(value); + if (i <= 0) { + throw new NumberFormatException("Value not positive."); + } + return i; + } catch (NumberFormatException e) { + LOGGER.log( + Level.CONFIG, + "Value of 'jersey.config.test.container.port'" + + " property is not a valid positive integer [" + value + "]." + + " Reverting to default [" + DEFAULT_PORT + "].", + e); + } + } + + return DEFAULT_PORT; + } + + private volatile SimpleServer server; + + public UriBuilder getUri() { + return UriBuilder.fromUri("http://localhost").port(getPort()).path(CONTEXT); + } + + public void startServer(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + config.register(LoggingFeature.class); + final URI baseUri = getBaseUri(); + server = SimpleContainerFactory.create(baseUri, config); + LOGGER.log(Level.INFO, "Simple-http server started on base uri: " + baseUri); + } + + public void startServerNoLoggingFilter(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + final URI baseUri = getBaseUri(); + server = SimpleContainerFactory.create(baseUri, config); + LOGGER.log(Level.INFO, "Simple-http server started on base uri: " + baseUri); + } + + public void startServer(ResourceConfig config) { + final URI baseUri = getBaseUri(); + config.register(LoggingFeature.class); + server = SimpleContainerFactory.create(baseUri, config); + LOGGER.log(Level.INFO, "Simple-http server started on base uri: " + baseUri); + } + + public void startServer(ResourceConfig config, int count, int select) { + final URI baseUri = getBaseUri(); + config.register(LoggingFeature.class); + server = SimpleContainerFactory.create(baseUri, config, count, select); + LOGGER.log(Level.INFO, "Simple-http server started on base uri: " + baseUri); + } + + public URI getBaseUri() { + return UriBuilder.fromUri("http://localhost/").port(getPort()).build(); + } + + public void setDebug(boolean enable) { + if (server != null) { + server.setDebug(enable); + } + } + + public void stopServer() { + try { + server.close(); + server = null; + LOGGER.log(Level.INFO, "Simple-http server stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @After + public void tearDown() { + if (server != null) { + stopServer(); + } + } +}
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/AsyncTest.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/AsyncTest.java new file mode 100644 index 0000000..9587b04 --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/AsyncTest.java
@@ -0,0 +1,175 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.container.TimeoutHandler; +import javax.ws.rs.core.Response; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Michal Gajdos + */ +public class AsyncTest extends AbstractSimpleServerTester { + + @Path("/async") + @SuppressWarnings("VoidMethodAnnotatedWithGET") + public static class AsyncResource { + + public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0); + + @GET + public void asyncGet(@Suspended final AsyncResponse asyncResponse) { + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 5 seconds, simulated using + // sleep() + try { + Thread.sleep(5000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(final AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + } + }); + asyncResponse.setTimeout(3, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 10 seconds, simulated using + // sleep() + try { + Thread.sleep(7000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("multiple-invocations") + public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) { + INVOCATION_COUNT.incrementAndGet(); + + new Thread(new Runnable() { + @Override + public void run() { + asyncResponse.resume("OK"); + } + }).start(); + } + } + + private Client client; + + @Before + public void setUp() throws Exception { + startServer(AsyncResource.class); + client = ClientBuilder.newClient(); + } + + @Override + @After + public void tearDown() { + super.tearDown(); + client = null; + } + + @Test + public void testAsyncGet() throws ExecutionException, InterruptedException { + final Future<Response> responseFuture = + client.target(getUri().path("/async")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + // get() waits for the response + assertEquals("DONE", response.readEntity(String.class)); + } + + @Test + public void testAsyncGetWithTimeout() + throws ExecutionException, InterruptedException, TimeoutException { + final Future<Response> responseFuture = + client.target(getUri().path("/async/timeout")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } + + /** + * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request. + */ + @Test + public void testAsyncMultipleInvocations() throws Exception { + final Response response = + client.target(getUri().path("/async/multiple-invocations")).request().get(); + + assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1)); + + assertThat(response.getStatus(), is(200)); + assertThat(response.readEntity(String.class), is("OK")); + } +}
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/ExceptionTest.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/ExceptionTest.java new file mode 100644 index 0000000..15e7b28 --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/ExceptionTest.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Paul Sandoz + */ +public class ExceptionTest extends AbstractSimpleServerTester { + @Path("{status}") + public static class ExceptionResource { + @GET + public String get(@PathParam("status") int status) { + throw new WebApplicationException(status); + } + + } + + @Test + public void test400StatusCode() { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("400").build()); + assertEquals(400, r.request().get(Response.class).getStatus()); + } + + @Test + public void test500StatusCode() { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("500").build()); + + assertEquals(500, r.request().get(Response.class).getStatus()); + } +}
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/LifecycleListenerTest.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/LifecycleListenerTest.java new file mode 100644 index 0000000..439b7d2 --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/LifecycleListenerTest.java
@@ -0,0 +1,130 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener; +import org.glassfish.jersey.server.spi.Container; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + + +/** + * Reload and ContainerLifecycleListener support test. + * + * @author Paul Sandoz + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class LifecycleListenerTest extends AbstractSimpleServerTester { + + @Path("/one") + public static class One { + @GET + public String get() { + return "one"; + } + } + + @Path("/two") + public static class Two { + @GET + public String get() { + return "two"; + } + } + + public static class Reloader extends AbstractContainerLifecycleListener { + Container container; + + public void reload(ResourceConfig newConfig) { + container.reload(newConfig); + } + + public void reload() { + container.reload(); + } + + @Override + public void onStartup(Container container) { + this.container = container; + } + + } + + @Test + public void testReload() { + final ResourceConfig rc = new ResourceConfig(One.class); + + Reloader reloader = new Reloader(); + rc.registerInstances(reloader); + + startServer(rc); + + WebTarget r = ClientBuilder.newClient().target(getUri().path("/").build()); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals(404, r.path("two").request().get(Response.class).getStatus()); + + // add Two resource + reloader.reload(new ResourceConfig(One.class, Two.class)); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals("two", r.path("two").request().get(String.class)); + } + + static class StartStopListener extends AbstractContainerLifecycleListener { + volatile boolean started; + volatile boolean stopped; + + @Override + public void onStartup(Container container) { + started = true; + } + + @Override + public void onShutdown(Container container) { + stopped = true; + } + } + + @Test + public void testStartupShutdownHooks() { + final StartStopListener listener = new StartStopListener(); + + startServer(new ResourceConfig(One.class).register(listener)); + + WebTarget r = ClientBuilder.newClient().target(getUri().path("/").build()); + + assertThat(r.path("one").request().get(String.class), equalTo("one")); + assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404)); + + stopServer(); + + assertTrue("ContainerLifecycleListener.onStartup has not been called.", listener.started); + assertTrue("ContainerLifecycleListener.onShutdown has not been called.", listener.stopped); + } +}
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/OptionsTest.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/OptionsTest.java new file mode 100644 index 0000000..3b12b1a --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/OptionsTest.java
@@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class OptionsTest extends AbstractSimpleServerTester { + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + } + + @Path("/users") + public class UserResource { + + @Path("/current") + @GET + @Produces("text/plain") + public String getCurrentUser() { + return "current user"; + } + } + + private Client client; + + @Before + public void setUp() throws Exception { + startServer(HelloWorldResource.class, UserResource.class); + client = ClientBuilder.newClient(); + } + + @Override + @After + public void tearDown() { + super.tearDown(); + client = null; + } + + + @Test + public void testFooBarOptions() { + Response response = + client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(0, response.getLength()); + assertEquals("foo/bar", response.getMediaType().toString()); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testNoDefaultMethod() { + Response response = client.target(getUri()).path("/users").request().options(); + assertThat(response.getStatus(), is(404)); + } + +}
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/ParallelTest.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/ParallelTest.java new file mode 100644 index 0000000..e0b6611 --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/ParallelTest.java
@@ -0,0 +1,112 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Tests the parallel execution of multiple requests. + * + * @author Stepan Kopriva + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class ParallelTest extends AbstractSimpleServerTester { + + // Server-side dispatcher and selector pool configuration + private static final int selectorThreads = Runtime.getRuntime().availableProcessors(); + private static final int dispatcherThreads = Math.max(8, selectorThreads * 2); + + private static final int numberOfThreads = 100; + + private static final String PATH = "test"; + private static AtomicInteger receivedCounter = new AtomicInteger(0); + private static AtomicInteger resourceCounter = new AtomicInteger(0); + private static CountDownLatch latch = new CountDownLatch(numberOfThreads); + + @Path(PATH) + public static class MyResource { + + @GET + public String get() { + this.sleep(); + resourceCounter.addAndGet(1); + return "GET"; + } + + private void sleep() { + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + Logger.getLogger(ParallelTest.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + private class ResourceThread extends Thread { + + private WebTarget target; + private String path; + + public ResourceThread(WebTarget target, String path) { + this.target = target; + this.path = path; + } + + @Override + public void run() { + assertEquals("GET", target.path(path).request().get(String.class)); + receivedCounter.addAndGet(1); + latch.countDown(); + } + } + + @Test + public void testParallel() { + ResourceConfig config = new ResourceConfig(MyResource.class); + startServer(config, dispatcherThreads, selectorThreads); + WebTarget target = ClientBuilder.newClient().target(getUri().path("/").build()); + + for (int i = 1; i <= numberOfThreads; i++) { + ResourceThread rt = new ResourceThread(target, PATH); + rt.start(); + } + + try { + latch.await(8000, TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + Logger.getLogger(ParallelTest.class.getName()).log(Level.SEVERE, null, ex); + } + + int result = receivedCounter.get(); + assertEquals(numberOfThreads, result); + } +}
diff --git a/containers/simple-http/src/test/java/org/glassfish/jersey/simple/TraceTest.java b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/TraceTest.java new file mode 100644 index 0000000..1e2d737 --- /dev/null +++ b/containers/simple-http/src/test/java/org/glassfish/jersey/simple/TraceTest.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.simple; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TraceTest extends AbstractSimpleServerTester { + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + } + + @Path("/users") + public class UserResource { + + @Path("/current") + @GET + @Produces("text/plain") + public String getCurrentUser() { + return "current user"; + } + } + + private Client client; + + @Before + public void setUp() throws Exception { + startServerNoLoggingFilter(HelloWorldResource.class, UserResource.class); // disable crude + // LoggingFilter + setDebug(true); + client = ClientBuilder.newClient(); + } + + @Override + @After + public void tearDown() { + super.tearDown(); + client = null; + } + + + @Test + public void testFooBarOptions() { + for (int i = 0; i < 100; i++) { + Response response = client.target(getUri()).path("helloworld").request() + .header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(0, response.getLength()); + assertEquals("foo/bar", response.getMediaType().toString()); + + try { + Thread.sleep(50); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testNoDefaultMethod() { + Response response = client.target(getUri()).path("/users").request().options(); + assertThat(response.getStatus(), is(404)); + } +}
diff --git a/core-client/pom.xml b/core-client/pom.xml new file mode 100644 index 0000000..8db5eab --- /dev/null +++ b/core-client/pom.xml
@@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <packaging>jar</packaging> + <name>jersey-core-client</name> + + <description>Jersey core client implementation</description> + + <build> + <plugins> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <inherited>false</inherited> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + <compilerArguments> + <!-- Do not warn about using sun.misc.Unsafe --> + <XDignore.symbol.file /> + </compilerArguments> + <showWarnings>false</showWarnings> + <fork>false</fork> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- Execute test classes in parallel - 1 thread per CPU core. --> + <parallel>classesAndMethods</parallel> + <perCoreThreadCount>true</perCoreThreadCount> + <threadCount>1</threadCount> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <profiles> + <profile> + <id>sonar</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- disable 'reuseForks' because 'AutoDiscoverableClientTest.testAutoDiscoverableClosing' test fails if executed together with JaCoCo agent with other tests in a single JVM --> + <reuseForks>false</reuseForks> + + <!-- disable JaCoCo listener because with switched 'reuseForks' to 'false' (due to + https://jira.sonarsource.com/browse/SONARJAVA-728 (https://github.com/SonarSource/sonar-java/pull/324) --> + <properties combine.self="override" /> + + <!-- parallel execution doesn't need to be disabled since JaCoCo listener is disabled --> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + +</project>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/AbortException.java b/core-client/src/main/java/org/glassfish/jersey/client/AbortException.java new file mode 100644 index 0000000..0a81aff --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/AbortException.java
@@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import javax.ws.rs.ProcessingException; + +/** + * Internal exception indicating that request processing has been aborted + * in the request filter processing chain. + * + * @author Marek Potociar (marek.potociar at oracle.com) + * + * @see javax.ws.rs.client.ClientRequestContext#abortWith(javax.ws.rs.core.Response) + */ +class AbortException extends ProcessingException { + private final transient ClientResponse abortResponse; + + /** + * Create new abort exception. + * + * @param abortResponse abort response. + */ + AbortException(ClientResponse abortResponse) { + super("Request processing has been aborted"); + this.abortResponse = abortResponse; + } + + /** + * Get the abort response that caused this exception. + * + * @return abort response. + */ + public ClientResponse getAbortResponse() { + return abortResponse; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/AbstractRxInvoker.java b/core-client/src/main/java/org/glassfish/jersey/client/AbstractRxInvoker.java new file mode 100644 index 0000000..deb4bd0 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/AbstractRxInvoker.java
@@ -0,0 +1,184 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.concurrent.ExecutorService; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.RxInvoker; +import javax.ws.rs.client.SyncInvoker; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; + +/** + * Default implementation of {@link javax.ws.rs.client.rx.RxInvoker reactive invoker}. Extensions of this class are + * supposed to implement {@link #method(String, Entity, Class)} and + * {@link #method(String, Entity, GenericType)} methods to which implementations of the rest + * of the methods from the contract delegate to. + * + * @param <T> the asynchronous/event-based completion aware type. The given type should be parametrized with the actual + * response type. + * @author Michal Gajdos + * @since 2.26 + */ +public abstract class AbstractRxInvoker<T> implements RxInvoker<T> { + + private final SyncInvoker syncInvoker; + private final ExecutorService executorService; + + public AbstractRxInvoker(final SyncInvoker syncInvoker, final ExecutorService executor) { + if (syncInvoker == null) { + throw new IllegalArgumentException("Invocation builder cannot be null."); + } + + this.syncInvoker = syncInvoker; + this.executorService = executor; + } + + /** + * Return invocation builder this reactive invoker was initialized with. + * + * @return non-null invocation builder. + */ + protected SyncInvoker getSyncInvoker() { + return syncInvoker; + } + + /** + * Return executorService service this reactive invoker was initialized with. + * + * @return executorService service instance or {@code null}. + */ + protected ExecutorService getExecutorService() { + return executorService; + } + + @Override + public T get() { + return method("GET"); + } + + @Override + public <R> T get(final Class<R> responseType) { + return method("GET", responseType); + } + + @Override + public <R> T get(final GenericType<R> responseType) { + return method("GET", responseType); + } + + @Override + public T put(final Entity<?> entity) { + return method("PUT", entity); + } + + @Override + public <R> T put(final Entity<?> entity, final Class<R> clazz) { + return method("PUT", entity, clazz); + } + + @Override + public <R> T put(final Entity<?> entity, final GenericType<R> type) { + return method("PUT", entity, type); + } + + @Override + public T post(final Entity<?> entity) { + return method("POST", entity); + } + + @Override + public <R> T post(final Entity<?> entity, final Class<R> clazz) { + return method("POST", entity, clazz); + } + + @Override + public <R> T post(final Entity<?> entity, final GenericType<R> type) { + return method("POST", entity, type); + } + + @Override + public T delete() { + return method("DELETE"); + } + + @Override + public <R> T delete(final Class<R> responseType) { + return method("DELETE", responseType); + } + + @Override + public <R> T delete(final GenericType<R> responseType) { + return method("DELETE", responseType); + } + + @Override + public T head() { + return method("HEAD"); + } + + @Override + public T options() { + return method("OPTIONS"); + } + + @Override + public <R> T options(final Class<R> responseType) { + return method("OPTIONS", responseType); + } + + @Override + public <R> T options(final GenericType<R> responseType) { + return method("OPTIONS", responseType); + } + + @Override + public T trace() { + return method("TRACE"); + } + + @Override + public <R> T trace(final Class<R> responseType) { + return method("TRACE", responseType); + } + + @Override + public <R> T trace(final GenericType<R> responseType) { + return method("TRACE", responseType); + } + + @Override + public T method(final String name) { + return method(name, Response.class); + } + + @Override + public <R> T method(final String name, final Class<R> responseType) { + return method(name, null, responseType); + } + + @Override + public <R> T method(final String name, final GenericType<R> responseType) { + return method(name, null, responseType); + } + + @Override + public T method(final String name, final Entity<?> entity) { + return method(name, entity, Response.class); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ChunkParser.java b/core-client/src/main/java/org/glassfish/jersey/client/ChunkParser.java new file mode 100644 index 0000000..9aa3ca7 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ChunkParser.java
@@ -0,0 +1,54 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Chunk data parser. + * + * Implementations of this interface are used by a {@link org.glassfish.jersey.client.ChunkedInput} + * instance for parsing response entity input stream into chunks. + * <p> + * Chunk parsers are expected to read data from the response entity input stream + * until a non-empty data chunk is fully read and then return the chunk data back + * to the {@link org.glassfish.jersey.client.ChunkedInput} instance for further + * processing (i.e. conversion into a specific Java type). + * </p> + * <p> + * Chunk parsers are typically expected to skip any empty chunks (the chunks that do + * not contain any data) or any control meta-data associated with chunks, however it + * is not a hard requirement to do so. The decision depends on the knowledge of which + * {@link javax.ws.rs.ext.MessageBodyReader} implementation is selected for de-serialization + * of the chunk data. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public interface ChunkParser { + /** + * Invoked by {@link org.glassfish.jersey.client.ChunkedInput} to get the data for + * the next chunk. + * + * @param responseStream response entity input stream. + * @return next chunk data represented as an array of bytes, or {@code null} + * if no more chunks are available. + * @throws java.io.IOException in case reading from the response entity fails. + */ + public byte[] readChunk(InputStream responseStream) throws IOException; +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInput.java b/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInput.java new file mode 100644 index 0000000..e7101c5 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInput.java
@@ -0,0 +1,498 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.ReaderInterceptor; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.message.MessageBodyWorkers; + +/** + * Response entity type used for receiving messages in "typed" chunks. + * <p/> + * This data type is useful for consuming partial responses from large or continuous data + * input streams. + * + * @param <T> chunk type. + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@SuppressWarnings("UnusedDeclaration") +public class ChunkedInput<T> extends GenericType<T> implements Closeable { + + private static final Logger LOGGER = Logger.getLogger(ChunkedInput.class.getName()); + + private final AtomicBoolean closed = new AtomicBoolean(false); + private ChunkParser parser = createParser("\r\n"); + private MediaType mediaType; + + private final InputStream inputStream; + private final Annotation[] annotations; + private final MultivaluedMap<String, String> headers; + private final MessageBodyWorkers messageBodyWorkers; + private final PropertiesDelegate propertiesDelegate; + + /** + * Create new chunk parser that will split the response entity input stream + * based on a fixed boundary string. + * + * @param boundary chunk boundary. + * @return new fixed boundary string-based chunk parser. + */ + public static ChunkParser createParser(final String boundary) { + return new FixedBoundaryParser(boundary.getBytes()); + } + + /** + * Create new chunk parser that will split the response entity input stream + * based on a fixed boundary sequence of bytes. + * + * @param boundary chunk boundary. + * @return new fixed boundary sequence-based chunk parser. + */ + public static ChunkParser createParser(final byte[] boundary) { + return new FixedBoundaryParser(boundary); + } + + /** + * Create a new chunk multi-parser that will split the response entity input stream + * based on multiple fixed boundary strings. + * + * @param boundaries chunk boundaries. + * @return new fixed boundary string-based chunk parser. + */ + public static ChunkParser createMultiParser(final String... boundaries) { + return new FixedMultiBoundaryParser(boundaries); + } + + private abstract static class AbstractBoundaryParser implements ChunkParser { + + @Override + public byte[] readChunk(final InputStream in) throws IOException { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] delimiterBuffer = new byte[getDelimiterBufferSize()]; + + int data; + int dPos; + do { + dPos = 0; + while ((data = in.read()) != -1) { + final byte b = (byte) data; + byte[] delimiter = getDelimiter(b, dPos, delimiterBuffer); + + // last read byte is part of the chunk delimiter + if (delimiter != null && b == delimiter[dPos]) { + delimiterBuffer[dPos++] = b; + if (dPos == delimiter.length) { + // found chunk delimiter + break; + } + } else if (dPos > 0) { + delimiter = getDelimiter(dPos - 1, delimiterBuffer); + delimiterBuffer[dPos] = b; + + int matched = matchTail(delimiterBuffer, 1, dPos, delimiter); + if (matched == 0) { + // flush delimiter buffer + buffer.write(delimiterBuffer, 0, dPos); + buffer.write(b); + dPos = 0; + } else if (matched == delimiter.length) { + // found chunk delimiter + break; + } else { + // one or more elements of a previous buffered delimiter + // are parts of a current buffered delimiter + buffer.write(delimiterBuffer, 0, dPos + 1 - matched); + dPos = matched; + } + } else { + buffer.write(b); + } + } + + } while (data != -1 && buffer.size() == 0); // skip an empty chunk + + if (dPos > 0 && dPos != getDelimiter(dPos - 1, delimiterBuffer).length) { + // flush the delimiter buffer, if not empty - parsing finished in the middle of a potential delimiter sequence + buffer.write(delimiterBuffer, 0, dPos); + } + + return (buffer.size() > 0) ? buffer.toByteArray() : null; + } + + /** + * Selects a delimiter which corresponds to delimiter buffer. Method automatically appends {@code b} param on the + * {@code pos} position of {@code delimiterBuffer} array and then starts the selection process with a newly created array. + * + * @param b byte which will be added on the {@code pos} position of {@code delimiterBuffer} array + * @param pos number of bytes from the delimiter buffer which will be used in processing + * @param delimiterBuffer current content of the delimiter buffer + * @return delimiter which corresponds to delimiterBuffer + */ + abstract byte[] getDelimiter(byte b, int pos, byte[] delimiterBuffer); + + /** + * Selects a delimiter which corresponds to delimiter buffer. + * + * @param pos position of the last read byte + * @param delimiterBuffer number of bytes from the delimiter buffer which will be used in processing + * @return delimiter which corresponds to delimiterBuffer + */ + abstract byte[] getDelimiter(int pos, byte[] delimiterBuffer); + + /** + * Returns a delimiter buffer size depending on the selected strategy. + * <p> + * If a strategy has multiple registered delimiters, then the delimiter buffer should be a length of the longest + * delimiter. + * + * @return length of the delimiter buffer + */ + abstract int getDelimiterBufferSize(); + + /** + * Tries to find an element intersection between two arrays in a way that intersecting elements must be + * at the tail of the first array and at the beginning of the second array. + * <p> + * For example, consider the following two arrays: + * <pre> + * a1: {a, b, c, d, e} + * a2: {d, e, f, g} + * </pre> + * In this example, the intersection of tail of {@code a1} with head of {@code a2} is <tt>{d, e}</tt> + * and consists of 2 overlapping elements. + * </p> + * The method takes the first array represented as a sub-array in buffer demarcated by an offset and length. + * The second array is a fixed pattern to be matched. The method then compares the tail of the + * array in the buffer with the head of the pattern and returns the number of intersecting elements, + * or zero in case the two arrays do not intersect tail to head. + * + * @param buffer byte buffer containing the array whose tail to intersect. + * @param offset start of the array to be tail-matched in the {@code buffer}. + * @param length length of the array to be tail-matched. + * @param pattern pattern to be head-matched. + * @return {@code 0} if any part of the tail of the array in the buffer does not match + * any part of the head of the pattern, otherwise returns number of overlapping elements. + */ + private static int matchTail(byte[] buffer, int offset, int length, byte[] pattern) { + if (pattern == null) { + return 0; + } + + outer: + for (int i = 0; i < length; i++) { + final int tailLength = length - i; + for (int j = 0; j < tailLength; j++) { + if (buffer[offset + i + j] != pattern[j]) { + // mismatch - continue with shorter tail + continue outer; + } + } + + // found the longest matching tail + return tailLength; + } + return 0; + } + } + + private static class FixedBoundaryParser extends AbstractBoundaryParser { + + private final byte[] delimiter; + + public FixedBoundaryParser(final byte[] boundary) { + delimiter = Arrays.copyOf(boundary, boundary.length); + } + + @Override + byte[] getDelimiter(byte b, int pos, byte[] delimiterBuffer) { + return delimiter; + } + + @Override + byte[] getDelimiter(int pos, byte[] delimiterBuffer) { + return delimiter; + } + + @Override + int getDelimiterBufferSize() { + return delimiter.length; + } + } + + private static class FixedMultiBoundaryParser extends AbstractBoundaryParser { + + private final List<byte[]> delimiters = new ArrayList<byte[]>(); + + private final int longestDelimiterLength; + + public FixedMultiBoundaryParser(String... boundaries) { + for (String boundary: boundaries) { + byte[] boundaryBytes = boundary.getBytes(); + delimiters.add(Arrays.copyOf(boundaryBytes, boundaryBytes.length)); + } + + Collections.sort(delimiters, new Comparator<byte[]>() { + @Override + public int compare(byte[] o1, byte[] o2) { + return Integer.compare(o1.length, o2.length); + } + }); + + byte[] longestDelimiter = delimiters.get(delimiters.size() - 1); + this.longestDelimiterLength = longestDelimiter.length; + } + + @Override + byte[] getDelimiter(byte b, int pos, byte[] delimiterBuffer) { + byte[] buffer = Arrays.copyOf(delimiterBuffer, delimiterBuffer.length); + buffer[pos] = b; + + return getDelimiter(pos, buffer); + } + + @Override + byte[] getDelimiter(int pos, byte[] delimiterBuffer) { + outer: + for (byte[] delimiter: delimiters) { + if (pos > delimiter.length) { + continue; + } + + for (int i = 0; i <= pos && i < delimiter.length; i++) { + if (delimiter[i] != delimiterBuffer[i]) { + continue outer; + } else if (pos == i) { + return delimiter; + } + } + } + + return null; + } + + @Override + int getDelimiterBufferSize() { + return this.longestDelimiterLength; + } + } + + /** + * Package-private constructor used by the {@link ChunkedInputReader}. + * + * @param chunkType chunk type. + * @param inputStream response input stream. + * @param annotations annotations associated with response entity. + * @param mediaType response entity media type. + * @param headers response headers. + * @param messageBodyWorkers message body workers. + * @param propertiesDelegate properties delegate for this request/response. + */ + protected ChunkedInput( + final Type chunkType, + final InputStream inputStream, + final Annotation[] annotations, + final MediaType mediaType, + final MultivaluedMap<String, String> headers, + final MessageBodyWorkers messageBodyWorkers, + final PropertiesDelegate propertiesDelegate) { + super(chunkType); + + this.inputStream = inputStream; + this.annotations = annotations; + this.mediaType = mediaType; + this.headers = headers; + this.messageBodyWorkers = messageBodyWorkers; + this.propertiesDelegate = propertiesDelegate; + } + + /** + * Get the underlying chunk parser. + * <p> + * Note: Access to internal chunk parser is not a thread-safe operation and has to be explicitly synchronized + * in case the chunked input is used from multiple threads. + * </p> + * + * @return underlying chunk parser. + */ + public ChunkParser getParser() { + return parser; + } + + /** + * Set new chunk parser. + * <p> + * Note: Access to internal chunk parser is not a thread-safe operation and has to be explicitly synchronized + * in case the chunked input is used from multiple threads. + * </p> + * + * @param parser new chunk parser. + */ + public void setParser(final ChunkParser parser) { + this.parser = parser; + } + + /** + * Get chunk data media type. + * <p/> + * Default chunk data media type is derived from the value of the response + * <tt>{@value javax.ws.rs.core.HttpHeaders#CONTENT_TYPE}</tt> header field. + * This default value may be manually overridden by {@link #setChunkType(javax.ws.rs.core.MediaType) setting} + * a custom non-{@code null} chunk media type value. + * <p> + * Note: Access to internal chunk media type is not a thread-safe operation and has to + * be explicitly synchronized in case the chunked input is used from multiple threads. + * </p> + * + * @return media type specific to each chunk of data. + */ + public MediaType getChunkType() { + return mediaType; + } + + /** + * Set custom chunk data media type. + * <p/> + * By default, chunk data media type is derived from the value of the response + * <tt>{@value javax.ws.rs.core.HttpHeaders#CONTENT_TYPE}</tt> header field. + * Using this methods will override the default chunk media type value and set it + * to a custom non-{@code null} chunk media type. Once this method is invoked, + * all subsequent {@link #read chunk reads} will use the newly set chunk media + * type when selecting the proper {@link javax.ws.rs.ext.MessageBodyReader} for + * chunk de-serialization. + * <p> + * Note: Access to internal chunk media type is not a thread-safe operation and has to + * be explicitly synchronized in case the chunked input is used from multiple threads. + * </p> + * + * @param mediaType custom chunk data media type. Must not be {@code null}. + * @throws IllegalArgumentException in case the {@code mediaType} is {@code null}. + */ + public void setChunkType(final MediaType mediaType) throws IllegalArgumentException { + if (mediaType == null) { + throw new IllegalArgumentException(LocalizationMessages.CHUNKED_INPUT_MEDIA_TYPE_NULL()); + } + this.mediaType = mediaType; + } + + /** + * Set custom chunk data media type from a string value. + * <p> + * Note: Access to internal chunk media type is not a thread-safe operation and has to + * be explicitly synchronized in case the chunked input is used from multiple threads. + * </p> + * + * @param mediaType custom chunk data media type. Must not be {@code null}. + * @throws IllegalArgumentException in case the {@code mediaType} cannot be parsed into + * a valid {@link MediaType} instance or is {@code null}. + * @see #setChunkType(javax.ws.rs.core.MediaType) + */ + public void setChunkType(final String mediaType) throws IllegalArgumentException { + this.mediaType = MediaType.valueOf(mediaType); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + if (inputStream != null) { + try { + inputStream.close(); + } catch (final IOException e) { + LOGGER.log(Level.FINE, LocalizationMessages.CHUNKED_INPUT_STREAM_CLOSING_ERROR(), e); + } + } + } + } + + /** + * Check if the chunked input has been closed. + * + * @return {@code true} if this chunked input has been closed, {@code false} otherwise. + */ + public boolean isClosed() { + return closed.get(); + } + + /** + * Read next chunk from the response stream and convert it to a Java instance + * using the {@link #getChunkType() chunk media type}. The method returns {@code null} + * if the underlying entity input stream has been closed (either implicitly or explicitly + * by calling the {@link #close()} method). + * <p> + * Note: Access to internal chunk parser is not a thread-safe operation and has to be explicitly + * synchronized in case the chunked input is used from multiple threads. + * </p> + * + * @return next streamed chunk or {@code null} if the underlying entity input stream + * has been closed while reading next chunk data. + * @throws IllegalStateException in case this chunked input has been closed. + */ + @SuppressWarnings("unchecked") + public T read() throws IllegalStateException { + if (closed.get()) { + throw new IllegalStateException(LocalizationMessages.CHUNKED_INPUT_CLOSED()); + } + + try { + final byte[] chunk = parser.readChunk(inputStream); + if (chunk == null) { + close(); + } else { + final ByteArrayInputStream chunkStream = new ByteArrayInputStream(chunk); + // TODO: add interceptors: interceptors are used in ChunkedOutput, so the stream should + // be intercepted in the ChunkedInput too. Interceptors cannot be easily added to the readFrom + // method as they should wrap the stream before it is processed by ChunkParser. Also please check todo + // in ChunkedInput (this should be fixed together with this todo) + // issue: JERSEY-1809 + return (T) messageBodyWorkers.readFrom( + getRawType(), + getType(), + annotations, + mediaType, + headers, + propertiesDelegate, + chunkStream, + Collections.<ReaderInterceptor>emptyList(), + false); + } + } catch (final IOException e) { + Logger.getLogger(this.getClass().getName()).log(Level.FINE, e.getMessage(), e); + close(); + } + return null; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java b/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java new file mode 100644 index 0000000..2b4a9af --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java
@@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.message.MessageBodyWorkers; + +/** + * {@link javax.ws.rs.ext.MessageBodyWriter} for {@link ChunkedInput}. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@ConstrainedTo(RuntimeType.CLIENT) +class ChunkedInputReader implements MessageBodyReader<ChunkedInput> { + + @Inject + private Provider<MessageBodyWorkers> messageBodyWorkers; + @Inject + private Provider<PropertiesDelegate> propertiesDelegateProvider; + + @Override + public boolean isReadable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return aClass.equals(ChunkedInput.class); + } + + @Override + public ChunkedInput readFrom(Class<ChunkedInput> chunkedInputClass, + Type type, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap<String, String> headers, + InputStream inputStream) throws IOException, WebApplicationException { + + final Type chunkType = ReflectionHelper.getTypeArgument(type, 0); + + return new ChunkedInput( + chunkType, + inputStream, + annotations, + mediaType, + headers, + messageBodyWorkers.get(), + propertiesDelegateProvider.get()); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientAsyncExecutor.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientAsyncExecutor.java new file mode 100644 index 0000000..3d85ebc --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientAsyncExecutor.java
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Injection qualifier that can be used to inject an {@link java.util.concurrent.ExecutorService} + * instance used by Jersey client runtime to execute {@link javax.ws.rs.client.Invocation.Builder#async() asynchronous} + * client requests. + * <p> + * The asynchronous client request executor service instance injected using this injection qualifier can be customized + * by registering a custom {@link org.glassfish.jersey.spi.ExecutorServiceProvider} implementation that is itself annotated + * with the {@code @ClientAsyncExecutor} annotation. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @see org.glassfish.jersey.client.ClientAsyncExecutorLiteral + * @since 2.18 + */ +@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Qualifier +public @interface ClientAsyncExecutor { + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientAsyncExecutorLiteral.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientAsyncExecutorLiteral.java new file mode 100644 index 0000000..986460b --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientAsyncExecutorLiteral.java
@@ -0,0 +1,42 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import org.glassfish.jersey.internal.inject.AnnotationLiteral; + +/** + * {@link org.glassfish.jersey.client.ClientAsyncExecutor} annotation literal. + * <p> + * This class provides a {@link #INSTANCE constant instance} of the {@code @ClientAsyncExecutor} annotation to be used + * in method calls that require use of annotation instances. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.18 + */ +@SuppressWarnings("ClassExplicitlyAnnotation") +public final class ClientAsyncExecutorLiteral extends AnnotationLiteral<ClientAsyncExecutor> implements ClientAsyncExecutor { + + /** + * An {@link org.glassfish.jersey.client.ClientAsyncExecutor} annotation instance. + */ + public static final ClientAsyncExecutor INSTANCE = new ClientAsyncExecutorLiteral(); + + private ClientAsyncExecutorLiteral() { + // prevents instantiation from the outside. + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientBackgroundScheduler.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientBackgroundScheduler.java new file mode 100644 index 0000000..c01f066 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientBackgroundScheduler.java
@@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +/** + * Injection qualifier that can be used to inject an {@link java.util.concurrent.ScheduledExecutorService} + * instance used by Jersey client runtime to schedule background tasks. + * <p> + * The scheduled executor service instance injected using this injection qualifier can be customized + * by registering a custom {@link org.glassfish.jersey.spi.ScheduledExecutorServiceProvider} implementation that is itself + * annotated with the {@code @ClientAsyncExecutor} annotation. + * </p> + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + * @see ClientBackgroundSchedulerLiteral + * @since 2.26 + */ +@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Qualifier +public @interface ClientBackgroundScheduler { + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientBackgroundSchedulerLiteral.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientBackgroundSchedulerLiteral.java new file mode 100644 index 0000000..aeb4860 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientBackgroundSchedulerLiteral.java
@@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import org.glassfish.jersey.internal.inject.AnnotationLiteral; + +/** + * {@link ClientBackgroundScheduler} annotation literal. + * <p> + * This class provides a {@link #INSTANCE constant instance} of the {@code @ClientBackgroundScheduler} annotation to be used + * in method calls that require use of annotation instances. + * </p> + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + * @since 2.26 + */ +@SuppressWarnings("ClassExplicitlyAnnotation") +public final class ClientBackgroundSchedulerLiteral extends AnnotationLiteral<ClientBackgroundScheduler> + implements ClientBackgroundScheduler { + + /** + * An {@link ClientBackgroundScheduler} annotation instance. + */ + public static final ClientBackgroundScheduler INSTANCE = new ClientBackgroundSchedulerLiteral(); + + private ClientBackgroundSchedulerLiteral() { + // prevents instantiation from the outside. + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientBinder.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientBinder.java new file mode 100644 index 0000000..e30a258 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientBinder.java
@@ -0,0 +1,101 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Map; +import java.util.function.Supplier; + +import javax.ws.rs.RuntimeType; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.ext.MessageBodyReader; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.message.internal.MessagingBinders; +import org.glassfish.jersey.process.internal.RequestScoped; + +/** + * Registers all binders necessary for {@link Client} runtime. + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +class ClientBinder extends AbstractBinder { + + private final Map<String, Object> clientRuntimeProperties; + + private static class RequestContextInjectionFactory extends ReferencingFactory<ClientRequest> { + + @Inject + public RequestContextInjectionFactory(Provider<Ref<ClientRequest>> referenceFactory) { + super(referenceFactory); + } + } + + private static class PropertiesDelegateFactory implements Supplier<PropertiesDelegate> { + + private final Provider<ClientRequest> requestProvider; + + @Inject + private PropertiesDelegateFactory(Provider<ClientRequest> requestProvider) { + this.requestProvider = requestProvider; + } + + @Override + public PropertiesDelegate get() { + return requestProvider.get().getPropertiesDelegate(); + } + } + + /** + * Create new client binder for a new client runtime instance. + * + * @param clientRuntimeProperties map of client runtime properties. + */ + ClientBinder(Map<String, Object> clientRuntimeProperties) { + this.clientRuntimeProperties = clientRuntimeProperties; + } + + @Override + protected void configure() { + install(new MessagingBinders.MessageBodyProviders(clientRuntimeProperties, RuntimeType.CLIENT), + new MessagingBinders.HeaderDelegateProviders()); + + bindFactory(ReferencingFactory.referenceFactory()).to(new GenericType<Ref<ClientConfig>>() { + }).in(RequestScoped.class); + + bindFactory(RequestContextInjectionFactory.class) + .to(ClientRequest.class) + .in(RequestScoped.class); + + bindFactory(ReferencingFactory.referenceFactory()).to(new GenericType<Ref<ClientRequest>>() { + }).in(RequestScoped.class); + + bindFactory(PropertiesDelegateFactory.class, Singleton.class).to(PropertiesDelegate.class).in(RequestScoped.class); + + // ChunkedInput entity support + bind(ChunkedInputReader.class).to(MessageBodyReader.class).in(Singleton.class); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java new file mode 100644 index 0000000..fb2ea46 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java
@@ -0,0 +1,866 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +import javax.ws.rs.RuntimeType; +import javax.ws.rs.core.Configurable; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.ExtendedConfig; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.internal.AutoDiscoverableConfigurator; +import org.glassfish.jersey.internal.BootstrapBag; +import org.glassfish.jersey.internal.BootstrapConfigurator; +import org.glassfish.jersey.internal.ContextResolverFactory; +import org.glassfish.jersey.internal.ExceptionMapperFactory; +import org.glassfish.jersey.internal.JaxrsProviders; +import org.glassfish.jersey.internal.ServiceFinder; +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Injections; +import org.glassfish.jersey.internal.inject.ProviderBinder; +import org.glassfish.jersey.internal.spi.AutoDiscoverable; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.message.internal.MessageBodyFactory; +import org.glassfish.jersey.model.internal.CommonConfig; +import org.glassfish.jersey.model.internal.ComponentBag; +import org.glassfish.jersey.model.internal.ManagedObjectsFinalizer; +import org.glassfish.jersey.process.internal.RequestScope; + +/** + * Jersey externalized implementation of client-side JAX-RS {@link javax.ws.rs.core.Configurable + * configurable} contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Martin Matula + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +public class ClientConfig implements Configurable<ClientConfig>, ExtendedConfig { + /** + * Internal configuration state. + */ + private State state; + + private static class RuntimeConfigConfigurator implements BootstrapConfigurator { + + private final State runtimeConfig; + + private RuntimeConfigConfigurator(State runtimeConfig) { + this.runtimeConfig = runtimeConfig; + } + + @Override + public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + bootstrapBag.setConfiguration(runtimeConfig); + injectionManager.register(Bindings.service(runtimeConfig).to(Configuration.class)); + } + } + + /** + * Default encapsulation of the internal configuration state. + */ + private static class State implements Configurable<State>, ExtendedConfig { + + /** + * Strategy that returns the same state instance. + */ + private static final StateChangeStrategy IDENTITY = state -> state; + /** + * Strategy that returns a copy of the state instance. + */ + private static final StateChangeStrategy COPY_ON_CHANGE = State::copy; + + private volatile StateChangeStrategy strategy; + private final CommonConfig commonConfig; + private final JerseyClient client; + private volatile ConnectorProvider connectorProvider; + private volatile ExecutorService executorService; + private volatile ScheduledExecutorService scheduledExecutorService; + + private final LazyValue<ClientRuntime> runtime = Values.lazy((Value<ClientRuntime>) this::initRuntime); + + /** + * Configuration state change strategy. + */ + private interface StateChangeStrategy { + + /** + * Invoked whenever a mutator method is called in the given configuration + * state. + * + * @param state configuration state to be mutated. + * @return state instance that will be mutated and returned from the + * invoked configuration state mutator method. + */ + State onChange(final State state); + } + + /** + * Default configuration state constructor with {@link StateChangeStrategy "identity"} + * state change strategy. + * + * @param client bound parent Jersey client. + */ + State(final JerseyClient client) { + this.strategy = IDENTITY; + this.commonConfig = new CommonConfig(RuntimeType.CLIENT, ComponentBag.EXCLUDE_EMPTY); + this.client = client; + final Iterator<ConnectorProvider> iterator = ServiceFinder.find(ConnectorProvider.class).iterator(); + if (iterator.hasNext()) { + this.connectorProvider = iterator.next(); + } else { + this.connectorProvider = new HttpUrlConnectorProvider(); + } + } + + /** + * Copy the original configuration state while using the default state change + * strategy. + * + * @param client new Jersey client parent for the state. + * @param original configuration strategy to be copied. + */ + private State(final JerseyClient client, final State original) { + this.strategy = IDENTITY; + this.client = client; + this.commonConfig = new CommonConfig(original.commonConfig); + this.connectorProvider = original.connectorProvider; + this.executorService = original.executorService; + this.scheduledExecutorService = original.scheduledExecutorService; + } + + /** + * Create a copy of the configuration state within the same parent Jersey + * client instance scope. + * + * @return configuration state copy. + */ + State copy() { + return new State(this.client, this); + } + + /** + * Create a copy of the configuration state in a scope of the given + * parent Jersey client instance. + * + * @param client parent Jersey client instance. + * @return configuration state copy. + */ + State copy(final JerseyClient client) { + return new State(client, this); + } + + void markAsShared() { + strategy = COPY_ON_CHANGE; + } + + State preInitialize() { + final State state = strategy.onChange(this); + state.strategy = COPY_ON_CHANGE; + state.runtime.get().preInitialize(); + return state; + + } + + @Override + public State property(final String name, final Object value) { + final State state = strategy.onChange(this); + state.commonConfig.property(name, value); + return state; + } + + public State loadFrom(final Configuration config) { + final State state = strategy.onChange(this); + state.commonConfig.loadFrom(config); + return state; + } + + @Override + public State register(final Class<?> providerClass) { + final State state = strategy.onChange(this); + state.commonConfig.register(providerClass); + return state; + } + + @Override + public State register(final Object provider) { + final State state = strategy.onChange(this); + state.commonConfig.register(provider); + return state; + } + + @Override + public State register(final Class<?> providerClass, final int bindingPriority) { + final State state = strategy.onChange(this); + state.commonConfig.register(providerClass, bindingPriority); + return state; + } + + @Override + public State register(final Class<?> providerClass, final Class<?>... contracts) { + final State state = strategy.onChange(this); + state.commonConfig.register(providerClass, contracts); + return state; + } + + @Override + public State register(final Class<?> providerClass, final Map<Class<?>, Integer> contracts) { + final State state = strategy.onChange(this); + state.commonConfig.register(providerClass, contracts); + return state; + } + + @Override + public State register(final Object provider, final int bindingPriority) { + final State state = strategy.onChange(this); + state.commonConfig.register(provider, bindingPriority); + return state; + } + + @Override + public State register(final Object provider, final Class<?>... contracts) { + final State state = strategy.onChange(this); + state.commonConfig.register(provider, contracts); + return state; + } + + @Override + public State register(final Object provider, final Map<Class<?>, Integer> contracts) { + final State state = strategy.onChange(this); + state.commonConfig.register(provider, contracts); + return state; + } + + State connectorProvider(final ConnectorProvider provider) { + if (provider == null) { + throw new NullPointerException(LocalizationMessages.NULL_CONNECTOR_PROVIDER()); + } + final State state = strategy.onChange(this); + state.connectorProvider = provider; + return state; + } + + State executorService(final ExecutorService executorService) { + if (executorService == null) { + throw new NullPointerException(LocalizationMessages.NULL_EXECUTOR_SERVICE()); + } + final State state = strategy.onChange(this); + state.executorService = executorService; + return state; + } + + State scheduledExecutorService(final ScheduledExecutorService scheduledExecutorService) { + if (scheduledExecutorService == null) { + throw new NullPointerException(LocalizationMessages.NULL_SCHEDULED_EXECUTOR_SERVICE()); + } + final State state = strategy.onChange(this); + state.scheduledExecutorService = scheduledExecutorService; + return state; + } + + Connector getConnector() { + // Get the connector only if the runtime has been initialized. + return (runtime.isInitialized()) ? runtime.get().getConnector() : null; + } + + ConnectorProvider getConnectorProvider() { + return connectorProvider; + } + + ExecutorService getExecutorService() { + return executorService; + } + + ScheduledExecutorService getScheduledExecutorService() { + return scheduledExecutorService; + } + + JerseyClient getClient() { + return client; + } + + @Override + public State getConfiguration() { + return this; + } + + @Override + public RuntimeType getRuntimeType() { + return commonConfig.getConfiguration().getRuntimeType(); + } + + @Override + public Map<String, Object> getProperties() { + return commonConfig.getConfiguration().getProperties(); + } + + @Override + public Object getProperty(final String name) { + return commonConfig.getConfiguration().getProperty(name); + } + + @Override + public Collection<String> getPropertyNames() { + return commonConfig.getConfiguration().getPropertyNames(); + } + + @Override + public boolean isProperty(final String name) { + return commonConfig.getConfiguration().isProperty(name); + } + + @Override + public boolean isEnabled(final Feature feature) { + return commonConfig.getConfiguration().isEnabled(feature); + } + + @Override + public boolean isEnabled(final Class<? extends Feature> featureClass) { + return commonConfig.getConfiguration().isEnabled(featureClass); + } + + @Override + public boolean isRegistered(final Object component) { + return commonConfig.getConfiguration().isRegistered(component); + } + + @Override + public boolean isRegistered(final Class<?> componentClass) { + return commonConfig.getConfiguration().isRegistered(componentClass); + } + + @Override + public Map<Class<?>, Integer> getContracts(final Class<?> componentClass) { + return commonConfig.getConfiguration().getContracts(componentClass); + } + + @Override + public Set<Class<?>> getClasses() { + return commonConfig.getConfiguration().getClasses(); + } + + @Override + public Set<Object> getInstances() { + return commonConfig.getConfiguration().getInstances(); + } + + public void configureAutoDiscoverableProviders(InjectionManager injectionManager, + List<AutoDiscoverable> autoDiscoverables) { + commonConfig.configureAutoDiscoverableProviders(injectionManager, autoDiscoverables, false); + } + + public void configureForcedAutoDiscoverableProviders(InjectionManager injectionManager) { + commonConfig.configureAutoDiscoverableProviders(injectionManager, Collections.emptyList(), true); + } + + public void configureMetaProviders(InjectionManager injectionManager, ManagedObjectsFinalizer finalizer) { + commonConfig.configureMetaProviders(injectionManager, finalizer); + } + + public ComponentBag getComponentBag() { + return commonConfig.getComponentBag(); + } + + /** + * Initialize the newly constructed client instance. + */ + @SuppressWarnings("MethodOnlyUsedFromInnerClass") + private ClientRuntime initRuntime() { + /* + * Ensure that any attempt to add a new provider, feature, binder or modify the connector + * will cause a copy of the current state. + */ + markAsShared(); + + final State runtimeCfgState = this.copy(); + runtimeCfgState.markAsShared(); + + InjectionManager injectionManager = Injections.createInjectionManager(); + injectionManager.register(new ClientBinder(runtimeCfgState.getProperties())); + + BootstrapBag bootstrapBag = new BootstrapBag(); + bootstrapBag.setManagedObjectsFinalizer(new ManagedObjectsFinalizer(injectionManager)); + List<BootstrapConfigurator> bootstrapConfigurators = Arrays.asList( + new RequestScope.RequestScopeConfigurator(), + new RuntimeConfigConfigurator(runtimeCfgState), + new ContextResolverFactory.ContextResolversConfigurator(), + new MessageBodyFactory.MessageBodyWorkersConfigurator(), + new ExceptionMapperFactory.ExceptionMappersConfigurator(), + new JaxrsProviders.ProvidersConfigurator(), + new AutoDiscoverableConfigurator(RuntimeType.CLIENT)); + bootstrapConfigurators.forEach(configurator -> configurator.init(injectionManager, bootstrapBag)); + + // AutoDiscoverable. + if (!CommonProperties.getValue(runtimeCfgState.getProperties(), RuntimeType.CLIENT, + CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE, Boolean.FALSE, Boolean.class)) { + runtimeCfgState.configureAutoDiscoverableProviders(injectionManager, bootstrapBag.getAutoDiscoverables()); + } else { + runtimeCfgState.configureForcedAutoDiscoverableProviders(injectionManager); + } + + // Configure binders and features. + runtimeCfgState.configureMetaProviders(injectionManager, bootstrapBag.getManagedObjectsFinalizer()); + + // Bind providers. + ProviderBinder.bindProviders(runtimeCfgState.getComponentBag(), RuntimeType.CLIENT, null, injectionManager); + + ClientExecutorProvidersConfigurator executorProvidersConfigurator = + new ClientExecutorProvidersConfigurator(runtimeCfgState.getComponentBag(), + runtimeCfgState.client, + this.executorService, + this.scheduledExecutorService); + executorProvidersConfigurator.init(injectionManager, bootstrapBag); + + injectionManager.completeRegistration(); + + bootstrapConfigurators.forEach(configurator -> configurator.postInit(injectionManager, bootstrapBag)); + + final ClientConfig configuration = new ClientConfig(runtimeCfgState); + final Connector connector = connectorProvider.getConnector(client, configuration); + final ClientRuntime crt = new ClientRuntime(configuration, connector, injectionManager, bootstrapBag); + + client.registerShutdownHook(crt); + return crt; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final State state = (State) o; + + if (client != null ? !client.equals(state.client) : state.client != null) { + return false; + } + if (!commonConfig.equals(state.commonConfig)) { + return false; + } + return connectorProvider == null ? state.connectorProvider == null + : connectorProvider.equals(state.connectorProvider); + } + + @Override + public int hashCode() { + int result = commonConfig.hashCode(); + result = 31 * result + (client != null ? client.hashCode() : 0); + result = 31 * result + (connectorProvider != null ? connectorProvider.hashCode() : 0); + return result; + } + } + + /** + * Construct a new Jersey configuration instance with the default features + * and property values. + */ + public ClientConfig() { + this.state = new State(null); + } + + /** + * Construct a new Jersey configuration instance and register the provided list of provider classes. + * + * @param providerClasses provider classes to be registered with this client configuration. + */ + public ClientConfig(final Class<?>... providerClasses) { + this(); + for (final Class<?> providerClass : providerClasses) { + state.register(providerClass); + } + } + + /** + * Construct a new Jersey configuration instance and register the provided list of provider instances. + * + * @param providers provider instances to be registered with this client configuration. + */ + public ClientConfig(final Object... providers) { + this(); + for (final Object provider : providers) { + state.register(provider); + } + } + + /** + * Construct a new Jersey configuration instance with the features as well as + * property values copied from the supplied JAX-RS configuration instance. + * + * @param parent parent Jersey client instance. + */ + ClientConfig(final JerseyClient parent) { + this.state = new State(parent); + } + + /** + * Construct a new Jersey configuration instance with the features as well as + * property values copied from the supplied JAX-RS configuration instance. + * + * @param parent parent Jersey client instance. + * @param that original {@link javax.ws.rs.core.Configuration}. + */ + ClientConfig(final JerseyClient parent, final Configuration that) { + if (that instanceof ClientConfig) { + state = ((ClientConfig) that).state.copy(parent); + } else { + state = new State(parent); + state.loadFrom(that); + } + } + + /** + * Construct a new Jersey configuration instance using the supplied state. + * + * @param state to be referenced from the new configuration instance. + */ + private ClientConfig(final State state) { + this.state = state; + } + + /** + * Take a snapshot of the current configuration and its internal state. + * <p/> + * The returned configuration object is an new instance different from the + * original one, however the cloning of the internal configuration state is + * lazily deferred until either original or the snapshot configuration is + * modified for the first time since the snapshot was taken. + * + * @return snapshot of the current configuration. + */ + ClientConfig snapshot() { + state.markAsShared(); + return new ClientConfig(state); + } + + /** + * Load the internal configuration state from an externally provided configuration state. + * <p> + * Calling this method effectively replaces existing configuration state of the instance + * with the state represented by the externally provided configuration. + * + * @param config external configuration state to replace the configuration of this configurable + * instance. + * @return the updated client configuration instance. + */ + public ClientConfig loadFrom(final Configuration config) { + if (config instanceof ClientConfig) { + state = ((ClientConfig) config).state.copy(); + } else { + state.loadFrom(config); + } + return this; + } + + @Override + public ClientConfig register(final Class<?> providerClass) { + state = state.register(providerClass); + return this; + } + + @Override + public ClientConfig register(final Object provider) { + state = state.register(provider); + return this; + } + + @Override + public ClientConfig register(final Class<?> providerClass, final int bindingPriority) { + state = state.register(providerClass, bindingPriority); + return this; + } + + @Override + public ClientConfig register(final Class<?> providerClass, final Class<?>... contracts) { + state = state.register(providerClass, contracts); + return this; + } + + @Override + public ClientConfig register(final Class<?> providerClass, final Map<Class<?>, Integer> contracts) { + state = state.register(providerClass, contracts); + return this; + } + + @Override + public ClientConfig register(final Object provider, final int bindingPriority) { + state = state.register(provider, bindingPriority); + return this; + } + + @Override + public ClientConfig register(final Object provider, final Class<?>... contracts) { + state = state.register(provider, contracts); + return this; + } + + @Override + public ClientConfig register(final Object provider, final Map<Class<?>, Integer> contracts) { + state = state.register(provider, contracts); + return this; + } + + @Override + public ClientConfig property(final String name, final Object value) { + state = state.property(name, value); + return this; + } + + @Override + public ClientConfig getConfiguration() { + return this; + } + + @Override + public RuntimeType getRuntimeType() { + return state.getRuntimeType(); + } + + @Override + public Map<String, Object> getProperties() { + return state.getProperties(); + } + + @Override + public Object getProperty(final String name) { + return state.getProperty(name); + } + + @Override + public Collection<String> getPropertyNames() { + return state.getPropertyNames(); + } + + @Override + public boolean isProperty(final String name) { + return state.isProperty(name); + } + + @Override + public boolean isEnabled(final Feature feature) { + return state.isEnabled(feature); + } + + @Override + public boolean isEnabled(final Class<? extends Feature> featureClass) { + return state.isEnabled(featureClass); + } + + @Override + public boolean isRegistered(final Object component) { + return state.isRegistered(component); + } + + @Override + public Map<Class<?>, Integer> getContracts(final Class<?> componentClass) { + return state.getContracts(componentClass); + } + + @Override + public boolean isRegistered(final Class<?> componentClass) { + return state.isRegistered(componentClass); + } + + @Override + public Set<Class<?>> getClasses() { + return state.getClasses(); + } + + @Override + public Set<Object> getInstances() { + return state.getInstances(); + } + + /** + * Register a custom Jersey client connector provider. + * <p> + * The registered {@code ConnectorProvider} instance will provide a + * Jersey client {@link org.glassfish.jersey.client.spi.Connector} + * for the {@link org.glassfish.jersey.client.JerseyClient} instance + * created with this client configuration. + * </p> + * + * @param connectorProvider custom connector provider. Must not be {@code null}. + * @return this client config instance. + * @throws java.lang.NullPointerException in case the {@code connectorProvider} is {@code null}. + * @since 2.5 + */ + public ClientConfig connectorProvider(final ConnectorProvider connectorProvider) { + state = state.connectorProvider(connectorProvider); + return this; + } + + /** + * Register custom Jersey client async executor. + * + * @param executorService custom executor service instance + * @return this client config instance + */ + public ClientConfig executorService(final ExecutorService executorService) { + state = state.executorService(executorService); + return this; + } + + /** + * Register custom Jersey client scheduler. + * + * @param scheduledExecutorService custom scheduled executor service instance + * @return this client config instance + */ + public ClientConfig scheduledExecutorService(final ScheduledExecutorService scheduledExecutorService) { + state = state.scheduledExecutorService(scheduledExecutorService); + return this; + } + + /** + * Get the client transport connector. + * <p> + * May return {@code null} if no connector has been set. + * + * @return client transport connector or {code null} if not set. + */ + public Connector getConnector() { + return state.getConnector(); + } + + /** + * Get the client transport connector provider. + * <p> + * If no custom connector provider has been set, + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider default connector provider} + * instance is returned. + * + * @return configured client transport connector provider. + * @since 2.5 + */ + public ConnectorProvider getConnectorProvider() { + return state.getConnectorProvider(); + } + + /** + * Get custom client executor service. + * <p> + * May return null if no custom executor service has been set. + * + * @return custom executor service instance or {@code null} if not set. + * @since 2.26 + */ + public ExecutorService getExecutorService() { + return state.getExecutorService(); + } + + /** + * Get custom client scheduled executor service. + * <p> + * May return null if no custom scheduled executor service has been set. + * + * @return custom executor service instance or {@code null} if not set. + * @since 2.26 + */ + public ScheduledExecutorService getScheduledExecutorService() { + return state.getScheduledExecutorService(); + } + + /** + * Get the configured runtime. + * + * @return configured runtime. + */ + ClientRuntime getRuntime() { + return state.runtime.get(); + } + + public ClientExecutor getClientExecutor() { + return state.runtime.get(); + } + + /** + * Get the parent Jersey client this configuration is bound to. + * <p> + * May return {@code null} if no parent client has been bound. + * + * @return bound parent Jersey client or {@code null} if not bound. + */ + public JerseyClient getClient() { + return state.getClient(); + } + + + /** + * Pre initializes this configuration by initializing {@link ClientRuntime client runtime} + * including {@link org.glassfish.jersey.message.MessageBodyWorkers message body workers}. + * Once this method is called no other method implementing {@link Configurable} should be called + * on this pre initialized configuration otherwise configuration will change back to uninitialized. + * <p/> + * Note that this method must be called only when configuration is attached to the client. + * + * @return Client configuration. + */ + ClientConfig preInitialize() { + state = state.preInitialize(); + return this; + } + + /** + * Check that the configuration instance has a parent client set. + * + * @throws IllegalStateException in case no parent Jersey client has been + * bound to the configuration instance yet. + */ + void checkClient() throws IllegalStateException { + if (getClient() == null) { + throw new IllegalStateException("Client configuration does not contain a parent client instance."); + } + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ClientConfig other = (ClientConfig) obj; + return this.state == other.state || (this.state != null && this.state.equals(other.state)); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 47 * hash + (this.state != null ? this.state.hashCode() : 0); + return hash; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutor.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutor.java new file mode 100644 index 0000000..51cde1c --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutor.java
@@ -0,0 +1,92 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Executor for client async processing and background task scheduling. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + * @since 2.26 + */ +public interface ClientExecutor { + /** + * Submits a value-returning task for execution and returns a {@link Future} representing the pending results of the task. + * The Future's {@code get()} method will return the task's result upon successful completion. + * + * @param task task to submit + * @param <T> task's return type + * @return a {@code Future} representing pending completion of the task + * @throws {@link java.util.concurrent.RejectedExecutionException} if the task cannot be scheduled for execution + * @throws {@link NullPointerException} if the task is null + */ + <T> Future<T> submit(Callable<T> task); + + /** + * Submits a {@link Runnable} task for execution and returns a {@link Future} representing that task. The Future's {@code + * get()} method will return the given result upon successful completion. + * + * @param task the task to submit + * @return a {@code Future} representing pending completion of the task + * @throws {@link java.util.concurrent.RejectedExecutionException} if the task cannot be scheduled for execution + * @throws {@link NullPointerException} if the task is null + */ + Future<?> submit(Runnable task); + + /** + * Submits a {@link Runnable} task for execution and returns a {@link Future} representing that task. The Future's {@code + * get()} method will return the given result upon successful completion. + * + * @param task the task to submit + * @param result the result to return + * @param <T> result type + * @return a {@code Future} representing pending completion of the task + * @throws {@link java.util.concurrent.RejectedExecutionException} if the task cannot be scheduled for execution + * @throws {@link NullPointerException} if the task is null + */ + <T> Future<T> submit(Runnable task, T result); + + /** + * Creates and executes a {@link ScheduledFuture} that becomes enabled after the given delay. + * + * @param callable the function to execute + * @param delay the time from now to delay execution + * @param unit the time unit of the delay parameter + * @param <T> return type of the function + * @return a {@code ScheduledFuture} that can be used to extract result or cancel + * @throws {@link java.util.concurrent.RejectedExecutionException} if the task cannot be scheduled for execution + * @throws {@link NullPointerException} if callable is null + */ + <T> ScheduledFuture<T> schedule(Callable<T> callable, long delay, TimeUnit unit); + + /** + * Creates and executes a one-shot action that becomes enabled after the given delay. + * + * @param command the task to execute + * @param delay the time from now to delay execution + * @param unit the time unit of the daly parameter + * @return a scheduledFuture representing pending completion of the task and whose {@code get()} method will return {@code + * null} upon completion + */ + ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); + + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java new file mode 100644 index 0000000..220f4f7 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java
@@ -0,0 +1,226 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.reflect.Method; +import java.security.AccessController; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.glassfish.jersey.internal.BootstrapBag; +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InstanceBinding; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.model.internal.ComponentBag; +import org.glassfish.jersey.model.internal.ManagedObjectsFinalizer; +import org.glassfish.jersey.process.internal.AbstractExecutorProvidersConfigurator; +import org.glassfish.jersey.spi.ExecutorServiceProvider; +import org.glassfish.jersey.spi.ScheduledExecutorServiceProvider; + +/** + * Configurator which initializes and register {@link ExecutorServiceProvider} and + * {@link ScheduledExecutorServiceProvider}. + * + * @author Petr Bouda + */ +class ClientExecutorProvidersConfigurator extends AbstractExecutorProvidersConfigurator { + + private static final Logger LOGGER = Logger.getLogger(ClientExecutorProvidersConfigurator.class.getName()); + private static final ExecutorService MANAGED_EXECUTOR_SERVICE = lookupManagedExecutorService(); + + private final ComponentBag componentBag; + private final JerseyClient client; + private final ExecutorService customExecutorService; + private final ScheduledExecutorService customScheduledExecutorService; + + ClientExecutorProvidersConfigurator(ComponentBag componentBag, JerseyClient client, + ExecutorService customExecutorService, + ScheduledExecutorService customScheduledExecutorService) { + this.componentBag = componentBag; + this.client = client; + this.customExecutorService = customExecutorService; + this.customScheduledExecutorService = customScheduledExecutorService; + } + + @Override + public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + Map<String, Object> runtimeProperties = bootstrapBag.getConfiguration().getProperties(); + ManagedObjectsFinalizer finalizer = bootstrapBag.getManagedObjectsFinalizer(); + + ExecutorServiceProvider defaultAsyncExecutorProvider; + ScheduledExecutorServiceProvider defaultScheduledExecutorProvider; + + final ExecutorService clientExecutorService = client.getExecutorService() == null + // custom executor service can be also set via managed client config class, in that case, it ends up in the + // customExecutorService field (similar for scheduled version) + ? customExecutorService + : client.getExecutorService(); + + // if there is a users provided executor service, use it + if (clientExecutorService != null) { + defaultAsyncExecutorProvider = new ClientExecutorServiceProvider(clientExecutorService); + // otherwise, check for ClientProperties.ASYNC_THREADPOOL_SIZE - if that is set, Jersey will create the + // ExecutorService to be used. If not and running on Java EE container, ManagedExecutorService will be used. + // Final fallback is DefaultClientAsyncExecutorProvider with defined default. + } else { + // Default async request executors support + Integer asyncThreadPoolSize = ClientProperties + .getValue(runtimeProperties, ClientProperties.ASYNC_THREADPOOL_SIZE, Integer.class); + + if (asyncThreadPoolSize != null) { + // TODO: Do we need to register DEFAULT Executor and ScheduledExecutor to InjectionManager? + asyncThreadPoolSize = (asyncThreadPoolSize < 0) ? 0 : asyncThreadPoolSize; + InstanceBinding<Integer> asyncThreadPoolSizeBinding = Bindings + .service(asyncThreadPoolSize) + .named("ClientAsyncThreadPoolSize"); + injectionManager.register(asyncThreadPoolSizeBinding); + + defaultAsyncExecutorProvider = new DefaultClientAsyncExecutorProvider(asyncThreadPoolSize); + } else { + if (MANAGED_EXECUTOR_SERVICE != null) { + defaultAsyncExecutorProvider = new ClientExecutorServiceProvider(MANAGED_EXECUTOR_SERVICE); + } else { + defaultAsyncExecutorProvider = new DefaultClientAsyncExecutorProvider(0); + } + } + } + + InstanceBinding<ExecutorServiceProvider> executorBinding = Bindings + .service(defaultAsyncExecutorProvider) + .to(ExecutorServiceProvider.class); + + injectionManager.register(executorBinding); + finalizer.registerForPreDestroyCall(defaultAsyncExecutorProvider); + + final ScheduledExecutorService clientScheduledExecutorService = client.getScheduledExecutorService() == null + // scheduled executor service set from {@link ClientConfig}. + ? customScheduledExecutorService + : client.getScheduledExecutorService(); + + if (clientScheduledExecutorService != null) { + defaultScheduledExecutorProvider = + new ClientScheduledExecutorServiceProvider(Values.of(clientScheduledExecutorService)); + } else { + ScheduledExecutorService scheduledExecutorService = lookupManagedScheduledExecutorService(); + defaultScheduledExecutorProvider = + scheduledExecutorService == null + // default client background scheduler disposes the executor service when client is closed. + // we don't need to do that for user provided (via ClientBuilder) or managed executor service. + ? new DefaultClientBackgroundSchedulerProvider() + : new ClientScheduledExecutorServiceProvider(Values.of(scheduledExecutorService)); + } + + InstanceBinding<ScheduledExecutorServiceProvider> schedulerBinding = Bindings + .service(defaultScheduledExecutorProvider) + .to(ScheduledExecutorServiceProvider.class); + injectionManager.register(schedulerBinding); + finalizer.registerForPreDestroyCall(defaultScheduledExecutorProvider); + + registerExecutors(injectionManager, componentBag, defaultAsyncExecutorProvider, defaultScheduledExecutorProvider); + } + + private static ExecutorService lookupManagedExecutorService() { + // Get the default ManagedExecutorService, if available + try { + // Android and some other environments don't have InitialContext class available. + final Class<?> aClass = + AccessController.doPrivileged(ReflectionHelper.classForNamePA("javax.naming.InitialContext")); + + final Object initialContext = aClass.newInstance(); + + final Method lookupMethod = aClass.getMethod("lookup", String.class); + return (ExecutorService) lookupMethod.invoke(initialContext, "java:comp/DefaultManagedExecutorService"); + } catch (Exception e) { + // ignore + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, e.getMessage(), e); + } + } catch (LinkageError error) { + // ignore - JDK8 compact2 profile - http://openjdk.java.net/jeps/161 + } + + return null; + } + + private ScheduledExecutorService lookupManagedScheduledExecutorService() { + try { + // Android and some other environments don't have InitialContext class available. + final Class<?> aClass = + AccessController.doPrivileged(ReflectionHelper.classForNamePA("javax.naming.InitialContext")); + final Object initialContext = aClass.newInstance(); + + final Method lookupMethod = aClass.getMethod("lookup", String.class); + return (ScheduledExecutorService) lookupMethod + .invoke(initialContext, "java:comp/DefaultManagedScheduledExecutorService"); + } catch (Exception e) { + // ignore + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, e.getMessage(), e); + } + } catch (LinkageError error) { + // ignore - JDK8 compact2 profile - http://openjdk.java.net/jeps/161 + } + + return null; + } + + @ClientAsyncExecutor + public static class ClientExecutorServiceProvider implements ExecutorServiceProvider { + + private final ExecutorService executorService; + + ClientExecutorServiceProvider(ExecutorService executorService) { + this.executorService = executorService; + } + + @Override + public ExecutorService getExecutorService() { + return executorService; + } + + @Override + public void dispose(ExecutorService executorService) { + + } + } + + @ClientBackgroundScheduler + public static class ClientScheduledExecutorServiceProvider implements ScheduledExecutorServiceProvider { + + private final Value<ScheduledExecutorService> executorService; + + ClientScheduledExecutorServiceProvider(Value<ScheduledExecutorService> executorService) { + this.executorService = executorService; + } + + @Override + public ScheduledExecutorService getExecutorService() { + return executorService.get(); + } + + @Override + public void dispose(ExecutorService executorService) { + + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java new file mode 100644 index 0000000..cb7efa3 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientFilteringStages.java
@@ -0,0 +1,119 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.ResponseProcessingException; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.model.internal.RankedComparator; +import org.glassfish.jersey.process.internal.AbstractChainableStage; +import org.glassfish.jersey.process.internal.ChainableStage; + +/** + * Client filtering stage factory. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +class ClientFilteringStages { + + private ClientFilteringStages() { + // Prevents instantiation + } + + /** + * Create client request filtering stage using the injection manager. May return {@code null}. + * + * @param injectionManager injection manager to be used. + * @return configured request filtering stage, or {@code null} in case there are no + * {@link ClientRequestFilter client request filters} registered in the injection manager. + */ + static ChainableStage<ClientRequest> createRequestFilteringStage(InjectionManager injectionManager) { + RankedComparator<ClientRequestFilter> comparator = new RankedComparator<>(RankedComparator.Order.ASCENDING); + Iterable<ClientRequestFilter> requestFilters = + Providers.getAllProviders(injectionManager, ClientRequestFilter.class, comparator); + return requestFilters.iterator().hasNext() ? new RequestFilteringStage(requestFilters) : null; + } + + /** + * Create client response filtering stage using the injection manager. May return {@code null}. + * + * @param injectionManager injection manager to be used. + * @return configured response filtering stage, or {@code null} in case there are no + * {@link ClientResponseFilter client response filters} registered in the injection manager. + */ + static ChainableStage<ClientResponse> createResponseFilteringStage(InjectionManager injectionManager) { + RankedComparator<ClientResponseFilter> comparator = new RankedComparator<>(RankedComparator.Order.DESCENDING); + Iterable<ClientResponseFilter> responseFilters = + Providers.getAllProviders(injectionManager, ClientResponseFilter.class, comparator); + return responseFilters.iterator().hasNext() ? new ResponseFilterStage(responseFilters) : null; + } + + private static final class RequestFilteringStage extends AbstractChainableStage<ClientRequest> { + + private final Iterable<ClientRequestFilter> requestFilters; + + private RequestFilteringStage(final Iterable<ClientRequestFilter> requestFilters) { + this.requestFilters = requestFilters; + } + + @Override + public Continuation<ClientRequest> apply(ClientRequest requestContext) { + for (ClientRequestFilter filter : requestFilters) { + try { + filter.filter(requestContext); + final Response abortResponse = requestContext.getAbortResponse(); + if (abortResponse != null) { + throw new AbortException(new ClientResponse(requestContext, abortResponse)); + } + } catch (IOException ex) { + throw new ProcessingException(ex); + } + } + return Continuation.of(requestContext, getDefaultNext()); + } + } + + private static class ResponseFilterStage extends AbstractChainableStage<ClientResponse> { + + private final Iterable<ClientResponseFilter> filters; + + private ResponseFilterStage(Iterable<ClientResponseFilter> filters) { + this.filters = filters; + } + + @Override + public Continuation<ClientResponse> apply(ClientResponse responseContext) { + try { + for (ClientResponseFilter filter : filters) { + filter.filter(responseContext.getRequestContext(), responseContext); + } + } catch (IOException ex) { + InboundJaxrsResponse response = new InboundJaxrsResponse(responseContext, null); + throw new ResponseProcessingException(response, ex); + } + + return Continuation.of(responseContext, getDefaultNext()); + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientLifecycleListener.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientLifecycleListener.java new file mode 100644 index 0000000..3aaf87f --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientLifecycleListener.java
@@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; + +import org.glassfish.jersey.spi.Contract; + +/** + * Jersey client lifecycle listener contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.11 + */ +@Contract +@ConstrainedTo(RuntimeType.CLIENT) +public interface ClientLifecycleListener { + + /** + * Invoked when a new runtime is initialized for the client instance. + */ + public void onInit(); + + /** + * Invoked when the client instance is closed. + */ + public void onClose(); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java new file mode 100644 index 0000000..e7b272f --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
@@ -0,0 +1,465 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Map; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.internal.HttpUrlConnector; +import org.glassfish.jersey.internal.util.PropertiesClass; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.internal.util.PropertyAlias; + +/** + * Jersey client implementation configuration properties. + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +@PropertiesClass +public final class ClientProperties { + + /** + * Automatic redirection. A value of {@code true} declares that the client + * will automatically redirect to the URI declared in 3xx responses. + * <p> + * The value MUST be an instance convertible to {@link java.lang.Boolean}. + * </p> + * <p> + * The default value is {@code true}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String FOLLOW_REDIRECTS = "jersey.config.client.followRedirects"; + + /** + * Read timeout interval, in milliseconds. + * <p> + * The value MUST be an instance convertible to {@link java.lang.Integer}. A + * value of zero (0) is equivalent to an interval of infinity. + * </p> + * <p> + * The default value is infinity (0). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String READ_TIMEOUT = "jersey.config.client.readTimeout"; + + /** + * Connect timeout interval, in milliseconds. + * <p> + * The value MUST be an instance convertible to {@link java.lang.Integer}. A + * value of zero (0) is equivalent to an interval of infinity. + * </p> + * <p> + * The default value is infinity (0). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String CONNECT_TIMEOUT = "jersey.config.client.connectTimeout"; + + /** + * The value MUST be an instance convertible to {@link java.lang.Integer}. + * <p> + * The property defines the size of the chunk in bytes. The property does not enable + * chunked encoding (it is controlled by {@link #REQUEST_ENTITY_PROCESSING} property). + * </p> + * <p> + * A default value is {@value #DEFAULT_CHUNK_SIZE} (since Jersey 2.16). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String CHUNKED_ENCODING_SIZE = "jersey.config.client.chunkedEncodingSize"; + /** + * Default chunk size in HTTP chunk-encoded messages. + * + * @since 2.16 + */ + public static final int DEFAULT_CHUNK_SIZE = 4096; + + /** + * Asynchronous thread pool size. + * <p> + * The value MUST be an instance of {@link java.lang.Integer}. + * </p> + * <p> + * If the property is absent then thread pool used for async requests will + * be initialized as default cached thread pool, which creates new thread + * for every new request, see {@link java.util.concurrent.Executors}. When a + * value > 0 is provided, the created cached thread pool limited to that + * number of threads will be utilized. Zero or negative values will be ignored. + * </p> + * <p> + * Note that the property may be ignored if a custom {@link org.glassfish.jersey.spi.ExecutorServiceProvider} + * is configured to execute asynchronous requests in the client runtime (see + * {@link org.glassfish.jersey.client.ClientAsyncExecutor}). + * </p> + * <p> + * A default value is not set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String ASYNC_THREADPOOL_SIZE = "jersey.config.client.async.threadPoolSize"; + + /** + * Scheduler thread pool size. + * <p> + * The value MUST be an instance of {@link java.lang.Integer}. + * </p> + * <p> + * If the property is absent then thread pool used for background task scheduling will + * be initialized as default scheduled thread pool executor, which creates new thread + * for every new request, see {@link java.util.concurrent.Executors}. When a + * value > 0 is provided, the created scheduled thread pool executor limited to that + * number of threads will be utilized. Zero or negative values will be ignored. + * </p> + * <p> + * Note that the property may be ignored if a custom {@link org.glassfish.jersey.spi.ExecutorServiceProvider} + * is configured to execute background tasks scheduling in the client runtime (see + * {@link org.glassfish.jersey.client.ClientBackgroundScheduler}). + * </p> + * <p> + * A default value is not set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String BACKGROUND_SCHEDULER_THREADPOOL_SIZE = "jersey.config.client.backgroundScheduler.threadPoolSize"; + + /** + * If {@link org.glassfish.jersey.client.filter.EncodingFilter} is + * registered, this property indicates the value of Content-Encoding + * property the filter should be adding. + * <p> + * The value MUST be an instance of {@link String}. + * </p> + * <p> + * The default value is {@code null}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String USE_ENCODING = "jersey.config.client.useEncoding"; + + /** + * If {@code true} then disable auto-discovery on the client. + * <p> + * By default auto-discovery on client is automatically enabled if global + * property + * {@value org.glassfish.jersey.CommonProperties#FEATURE_AUTO_DISCOVERY_DISABLE} + * is not disabled. If set then the client property value overrides the + * global property value. + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * <p>This constant is an alias for {@link CommonProperties#FEATURE_AUTO_DISCOVERY_DISABLE_CLIENT}.</p> + * + * @see org.glassfish.jersey.CommonProperties#FEATURE_AUTO_DISCOVERY_DISABLE + */ + @PropertyAlias + public static final String FEATURE_AUTO_DISCOVERY_DISABLE = CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE_CLIENT; + + /** + * An integer value that defines the buffer size used to buffer client-side + * request entity in order to determine its size and set the value of HTTP + * <tt>{@value javax.ws.rs.core.HttpHeaders#CONTENT_LENGTH}</tt> header. + * <p> + * If the entity size exceeds the configured buffer size, the buffering + * would be cancelled and the entity size would not be determined. Value + * less or equal to zero disable the buffering of the entity at all. + * </p> + * This property can be used on the client side to override the outbound + * message buffer size value - default or the global custom value set using + * the + * {@value org.glassfish.jersey.CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER} + * global property. + * <p> + * The default value is + * <tt>8192</tt>. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * <p>This constant is an alias for {@link CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER_CLIENT}.</p> + * + * @since 2.2 + */ + @PropertyAlias + public static final String OUTBOUND_CONTENT_LENGTH_BUFFER = CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_CLIENT; + + /** + * If {@code true} then disable configuration of Json Processing (JSR-353) + * feature on client. + * <p> + * By default Json Processing on client is automatically enabled if global + * property + * {@value org.glassfish.jersey.CommonProperties#JSON_PROCESSING_FEATURE_DISABLE} + * is not disabled. If set then the client property value overrides the + * global property value. + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * <p>This constant is an alias for {@link CommonProperties#JSON_PROCESSING_FEATURE_DISABLE_CLIENT}.</p> + * + * @see org.glassfish.jersey.CommonProperties#JSON_PROCESSING_FEATURE_DISABLE + */ + @PropertyAlias + public static final String JSON_PROCESSING_FEATURE_DISABLE = CommonProperties.JSON_PROCESSING_FEATURE_DISABLE_CLIENT; + + /** + * If {@code true} then disable META-INF/services lookup on client. + * <p> + * By default Jersey looks up SPI implementations described by {@code META-INF/services/*} files. + * Then you can register appropriate provider classes by {@link javax.ws.rs.core.Application}. + * </p> + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * <p>This constant is an alias for {@link CommonProperties#METAINF_SERVICES_LOOKUP_DISABLE_CLIENT}.</p> + * + * @see org.glassfish.jersey.CommonProperties#METAINF_SERVICES_LOOKUP_DISABLE + */ + @PropertyAlias + public static final String METAINF_SERVICES_LOOKUP_DISABLE = CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE_CLIENT; + + /** + * If {@code true} then disable configuration of MOXy Json feature on + * client. + * <p> + * By default MOXy Json on client is automatically enabled if global + * property + * {@value org.glassfish.jersey.CommonProperties#MOXY_JSON_FEATURE_DISABLE} + * is not disabled. If set then the client property value overrides the + * global property value. + * </p> + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * <p>This constant is an alias for {@link CommonProperties#MOXY_JSON_FEATURE_DISABLE_CLIENT}.</p> + * + * @see org.glassfish.jersey.CommonProperties#MOXY_JSON_FEATURE_DISABLE + * @since 2.1 + */ + @PropertyAlias + public static final String MOXY_JSON_FEATURE_DISABLE = CommonProperties.MOXY_JSON_FEATURE_DISABLE_CLIENT; + + /** + * If {@code true}, the strict validation of HTTP specification compliance + * will be suppressed. + * <p> + * By default, Jersey client runtime performs certain HTTP compliance checks + * (such as which HTTP methods can facilitate non-empty request entities + * etc.) in order to fail fast with an exception when user tries to + * establish a communication non-compliant with HTTP specification. Users + * who need to override these compliance checks and avoid the exceptions + * being thrown by Jersey client runtime for some reason, can set this + * property to {@code true}. As a result, the compliance issues will be + * merely reported in a log and no exceptions will be thrown. + * </p> + * <p> + * Note that the property suppresses the Jersey layer exceptions. Chances + * are that the non-compliant behavior will cause different set of + * exceptions being raised in the underlying I/O connector layer. + * </p> + * <p> + * This property can be configured in a client runtime configuration or + * directly on an individual request. In case of conflict, request-specific + * property value takes precedence over value configured in the runtime + * configuration. + * </p> + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @since 2.2 + */ + public static final String SUPPRESS_HTTP_COMPLIANCE_VALIDATION = + "jersey.config.client.suppressHttpComplianceValidation"; + + /** + * The property defines the size of digest cache in the + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#digest()} digest filter}. + * Cache contains authentication + * schemes for different request URIs. + * <p\> + * The value MUST be an instance of {@link java.lang.Integer} and it must be + * higher or equal to 1. + * </p> + * <p> + * The default value is {@code 1000}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @since 2.3 + */ + public static final String DIGESTAUTH_URI_CACHE_SIZELIMIT = "jersey.config.client.digestAuthUriCacheSizeLimit"; + + // TODO Need to implement support for PROXY-* properties in other connectors + /** + * The property defines a URI of a HTTP proxy the client connector should use. + * <p> + * If the port component of the URI is absent then a default port of {@code 8080} is assumed. + * If the property absent then no proxy will be utilized. + * </p> + * <p>The value MUST be an instance of {@link String}.</p> + * <p>The default value is {@code null}.</p> + * <p>The name of the configuration property is <tt>{@value}</tt>.</p> + * + * @since 2.5 + */ + public static final String PROXY_URI = "jersey.config.client.proxy.uri"; + + /** + * The property defines a user name which will be used for HTTP proxy authentication. + * <p> + * The property is ignored if no {@link #PROXY_URI HTTP proxy URI} has been set. + * If the property absent then no proxy authentication will be utilized. + * </p> + * <p>The value MUST be an instance of {@link String}.</p> + * <p>The default value is {@code null}.</p> + * <p>The name of the configuration property is <tt>{@value}</tt>.</p> + * + * @since 2.5 + */ + public static final String PROXY_USERNAME = "jersey.config.client.proxy.username"; + + /** + * The property defines a user password which will be used for HTTP proxy authentication. + * <p> + * The property is ignored if no {@link #PROXY_URI HTTP proxy URI} has been set. + * If the property absent then no proxy authentication will be utilized. + * </p> + * <p>The value MUST be an instance of {@link String}.</p> + * <p>The default value is {@code null}.</p> + * <p>The name of the configuration property is <tt>{@value}</tt>.</p> + * + * @since 2.5 + */ + public static final String PROXY_PASSWORD = "jersey.config.client.proxy.password"; + /** + * The property specified how the entity should be serialized to the output stream by the + * {@link org.glassfish.jersey.client.spi.Connector connector}; if the buffering + * should be used or the entity is streamed in chunked encoding. + * <p> + * The value MUST be an instance of {@link String} or an enum value {@link RequestEntityProcessing} in the case + * of programmatic definition of the property. Allowed values are: + * <ul> + * <li><b>{@code BUFFERED}</b>: the entity will be buffered and content length will be send in Content-length header.</li> + * <li><b>{@code CHUNKED}</b>: chunked encoding will be used and entity will be streamed.</li> + * </ul> + * </p> + * <p> + * Default value is {@code CHUNKED}. However, due to limitations some connectors can define different + * default value (usually if the chunked encoding cannot be properly supported on the {@code Connector}). + * This detail should be specified in the javadoc of particular connector. For example, {@link HttpUrlConnector} + * use buffering as the default mode. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @since 2.5 + */ + public static final String REQUEST_ENTITY_PROCESSING = "jersey.config.client.request.entity.processing"; + + private ClientProperties() { + // prevents instantiation + } + + /** + * Get the value of the specified property. + * <p> + * If the property is not set or the real value type is not compatible with + * {@code defaultValue} type, the specified {@code defaultValue} is returned. Calling this method is equivalent to calling + * <tt>ClientProperties.getValue(properties, key, defaultValue, (Class<T>) defaultValue.getClass())</tt>. + * </p> + * + * @param properties Map of properties to get the property value from. + * @param key Name of the property. + * @param defaultValue Default value if property is not registered + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, final String key, final T defaultValue) { + return PropertiesHelper.getValue(properties, key, defaultValue, null); + } + + /** + * Get the value of the specified property. + * <p/> + * If the property is not set or the real value type is not compatible with the specified value type, + * returns {@code defaultValue}. + * + * @param properties Map of properties to get the property value from. + * @param key Name of the property. + * @param defaultValue Default value if property is not registered + * @param type Type to retrieve the value as. + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, final String key, final T defaultValue, final Class<T> type) { + return PropertiesHelper.getValue(properties, key, defaultValue, type, null); + } + + /** + * Get the value of the specified property. + * <p/> + * If the property is not set or the actual property value type is not compatible with the specified type, the method will + * return {@code null}. + * + * @param properties Map of properties to get the property value from. + * @param key Name of the property. + * @param type Type to retrieve the value as. + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, final String key, final Class<T> type) { + return PropertiesHelper.getValue(properties, key, type, null); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java new file mode 100644 index 0000000..b58cbb5 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
@@ -0,0 +1,618 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Variant; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.WriterInterceptor; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.internal.guava.Preconditions; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManagerSupplier; +import org.glassfish.jersey.internal.util.ExceptionUtils; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.message.MessageBodyWorkers; +import org.glassfish.jersey.message.internal.OutboundMessageContext; + +/** + * Jersey client request context. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ClientRequest extends OutboundMessageContext implements ClientRequestContext, InjectionManagerSupplier { + + // Request-scoped configuration instance + private final ClientConfig clientConfig; + // Request-scoped properties delegate + private final PropertiesDelegate propertiesDelegate; + // Absolute request URI + private URI requestUri; + // Request method + private String httpMethod; + // Request filter chain execution aborting response + private Response abortResponse; + // Entity providers + private MessageBodyWorkers workers; + // Flag indicating whether the request is asynchronous + private boolean asynchronous; + // true if writeEntity() was already called + private boolean entityWritten; + // writer interceptors used to write the request + private Iterable<WriterInterceptor> writerInterceptors; + // reader interceptors used to write the request + private Iterable<ReaderInterceptor> readerInterceptors; + // do not add user-agent header (if not directly set) to the request. + private boolean ignoreUserAgent; + + private static final Logger LOGGER = Logger.getLogger(ClientRequest.class.getName()); + + /** + * Create new Jersey client request context. + * + * @param requestUri request Uri. + * @param clientConfig request configuration. + * @param propertiesDelegate properties delegate. + */ + protected ClientRequest( + final URI requestUri, final ClientConfig clientConfig, final PropertiesDelegate propertiesDelegate) { + clientConfig.checkClient(); + + this.requestUri = requestUri; + this.clientConfig = clientConfig; + this.propertiesDelegate = propertiesDelegate; + } + + /** + * Copy constructor. + * + * @param original original instance. + */ + public ClientRequest(final ClientRequest original) { + super(original); + this.requestUri = original.requestUri; + this.httpMethod = original.httpMethod; + this.workers = original.workers; + this.clientConfig = original.clientConfig.snapshot(); + this.asynchronous = original.isAsynchronous(); + this.readerInterceptors = original.readerInterceptors; + this.writerInterceptors = original.writerInterceptors; + this.propertiesDelegate = new MapPropertiesDelegate(original.propertiesDelegate); + this.ignoreUserAgent = original.ignoreUserAgent; + } + + /** + * Resolve a property value for the specified property {@code name}. + * + * <p> + * The method returns the value of the property registered in the request-specific + * property bag, if available. If no property for the given property name is found + * in the request-specific property bag, the method looks at the properties stored + * in the {@link #getConfiguration() global client-runtime configuration} this request + * belongs to. If there is a value defined in the client-runtime configuration, + * it is returned, otherwise the method returns {@code null} if no such property is + * registered neither in the client runtime nor in the request-specific property bag. + * </p> + * + * @param name property name. + * @param type expected property class type. + * @param <T> property Java type. + * @return resolved property value or {@code null} if no such property is registered. + */ + public <T> T resolveProperty(final String name, final Class<T> type) { + return resolveProperty(name, null, type); + } + + /** + * Resolve a property value for the specified property {@code name}. + * + * <p> + * The method returns the value of the property registered in the request-specific + * property bag, if available. If no property for the given property name is found + * in the request-specific property bag, the method looks at the properties stored + * in the {@link #getConfiguration() global client-runtime configuration} this request + * belongs to. If there is a value defined in the client-runtime configuration, + * it is returned, otherwise the method returns {@code defaultValue} if no such property is + * registered neither in the client runtime nor in the request-specific property bag. + * </p> + * + * @param name property name. + * @param defaultValue default value to return if the property is not registered. + * @param <T> property Java type. + * @return resolved property value or {@code defaultValue} if no such property is registered. + */ + @SuppressWarnings("unchecked") + public <T> T resolveProperty(final String name, final T defaultValue) { + return resolveProperty(name, defaultValue, (Class<T>) defaultValue.getClass()); + } + + private <T> T resolveProperty(final String name, Object defaultValue, final Class<T> type) { + // Check runtime configuration first + Object result = clientConfig.getProperty(name); + if (result != null) { + defaultValue = result; + } + + // Check request properties next + result = propertiesDelegate.getProperty(name); + if (result == null) { + result = defaultValue; + } + + return (result == null) ? null : PropertiesHelper.convertValue(result, type); + } + + @Override + public Object getProperty(final String name) { + return propertiesDelegate.getProperty(name); + } + + @Override + public Collection<String> getPropertyNames() { + return propertiesDelegate.getPropertyNames(); + } + + @Override + public void setProperty(final String name, final Object object) { + propertiesDelegate.setProperty(name, object); + } + + @Override + public void removeProperty(final String name) { + propertiesDelegate.removeProperty(name); + } + + /** + * Get the underlying properties delegate. + * + * @return underlying properties delegate. + */ + PropertiesDelegate getPropertiesDelegate() { + return propertiesDelegate; + } + + /** + * Get the underlying client runtime. + * + * @return underlying client runtime. + */ + ClientRuntime getClientRuntime() { + return clientConfig.getRuntime(); + } + + @Override + public URI getUri() { + return requestUri; + } + + @Override + public void setUri(final URI uri) { + this.requestUri = uri; + } + + @Override + public String getMethod() { + return httpMethod; + } + + @Override + public void setMethod(final String method) { + this.httpMethod = method; + } + + @Override + public JerseyClient getClient() { + return clientConfig.getClient(); + } + + @Override + public void abortWith(final Response response) { + this.abortResponse = response; + } + + /** + * Get the request filter chain aborting response if set, or {@code null} otherwise. + * + * @return request filter chain aborting response if set, or {@code null} otherwise. + */ + public Response getAbortResponse() { + return abortResponse; + } + + @Override + public Configuration getConfiguration() { + return clientConfig.getRuntime().getConfig(); + } + + /** + * Get internal client configuration state. + * + * @return internal client configuration state. + */ + ClientConfig getClientConfig() { + return clientConfig; + } + + @Override + public Map<String, Cookie> getCookies() { + return super.getRequestCookies(); + } + + /** + * Get the message body workers associated with the request. + * + * @return message body workers. + */ + public MessageBodyWorkers getWorkers() { + return workers; + } + + /** + * Set the message body workers associated with the request. + * + * @param workers message body workers. + */ + public void setWorkers(final MessageBodyWorkers workers) { + this.workers = workers; + } + + /** + * Add new accepted types to the message headers. + * + * @param types accepted types to be added. + */ + public void accept(final MediaType... types) { + getHeaders().addAll(HttpHeaders.ACCEPT, (Object[]) types); + } + + /** + * Add new accepted types to the message headers. + * + * @param types accepted types to be added. + */ + public void accept(final String... types) { + getHeaders().addAll(HttpHeaders.ACCEPT, (Object[]) types); + } + + /** + * Add new accepted languages to the message headers. + * + * @param locales accepted languages to be added. + */ + public void acceptLanguage(final Locale... locales) { + getHeaders().addAll(HttpHeaders.ACCEPT_LANGUAGE, (Object[]) locales); + } + + /** + * Add new accepted languages to the message headers. + * + * @param locales accepted languages to be added. + */ + public void acceptLanguage(final String... locales) { + getHeaders().addAll(HttpHeaders.ACCEPT_LANGUAGE, (Object[]) locales); + } + + /** + * Add new cookie to the message headers. + * + * @param cookie cookie to be added. + */ + public void cookie(final Cookie cookie) { + getHeaders().add(HttpHeaders.COOKIE, cookie); + } + + /** + * Add new cache control entry to the message headers. + * + * @param cacheControl cache control entry to be added. + */ + public void cacheControl(final CacheControl cacheControl) { + getHeaders().add(HttpHeaders.CACHE_CONTROL, cacheControl); + } + + /** + * Set message encoding. + * + * @param encoding message encoding to be set. + */ + public void encoding(final String encoding) { + if (encoding == null) { + getHeaders().remove(HttpHeaders.CONTENT_ENCODING); + } else { + getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, encoding); + } + } + + /** + * Set message language. + * + * @param language message language to be set. + */ + public void language(final String language) { + if (language == null) { + getHeaders().remove(HttpHeaders.CONTENT_LANGUAGE); + } else { + getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, language); + } + } + + /** + * Set message language. + * + * @param language message language to be set. + */ + public void language(final Locale language) { + if (language == null) { + getHeaders().remove(HttpHeaders.CONTENT_LANGUAGE); + } else { + getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, language); + } + } + + /** + * Set message content type. + * + * @param type message content type to be set. + */ + public void type(final MediaType type) { + setMediaType(type); + } + + /** + * Set message content type. + * + * @param type message content type to be set. + */ + public void type(final String type) { + type(type == null ? null : MediaType.valueOf(type)); + } + + /** + * Set message content variant (type, language and encoding). + * + * @param variant message content content variant (type, language and encoding) + * to be set. + */ + public void variant(final Variant variant) { + if (variant == null) { + type((MediaType) null); + language((String) null); + encoding(null); + } else { + type(variant.getMediaType()); + language(variant.getLanguage()); + encoding(variant.getEncoding()); + } + } + + /** + * Returns true if the request is called asynchronously using {@link javax.ws.rs.client.AsyncInvoker} + * + * @return True if the request is asynchronous; false otherwise. + */ + public boolean isAsynchronous() { + return asynchronous; + } + + /** + * Sets the flag indicating whether the request is called asynchronously using {@link javax.ws.rs.client.AsyncInvoker}. + * + * @param async True if the request is asynchronous; false otherwise. + */ + void setAsynchronous(final boolean async) { + asynchronous = async; + } + + /** + * Enable a buffering of serialized entity. The buffering will be configured from runtime configuration + * associated with this request. The property determining the size of the buffer + * is {@link org.glassfish.jersey.CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER}. + * <p/> + * The buffering functionality is by default disabled and could be enabled by calling this method. In this case + * this method must be called before first bytes are written to the {@link #getEntityStream() entity stream}. + * + */ + public void enableBuffering() { + enableBuffering(getConfiguration()); + } + + /** + * Write (serialize) the entity set in this request into the {@link #getEntityStream() entity stream}. The method + * use {@link javax.ws.rs.ext.WriterInterceptor writer interceptors} and {@link javax.ws.rs.ext.MessageBodyWriter + * message body writer}. + * <p/> + * This method modifies the state of this request and therefore it can be called only once per request life cycle otherwise + * IllegalStateException is thrown. + * <p/> + * Note that {@link #setStreamProvider(org.glassfish.jersey.message.internal.OutboundMessageContext.StreamProvider)} + * and optionally {@link #enableBuffering()} must be called before calling this method. + * + * @throws IOException In the case of IO error. + */ + public void writeEntity() throws IOException { + Preconditions.checkState(!entityWritten, LocalizationMessages.REQUEST_ENTITY_ALREADY_WRITTEN()); + entityWritten = true; + ensureMediaType(); + final GenericType<?> entityType = new GenericType(getEntityType()); + doWriteEntity(workers, entityType); + } + + /** + * Added only to make the code testable. + * + * @param writeWorkers Message body workers instance used to write the entity. + * @param entityType entity type. + * @throws IOException when {@link MessageBodyWorkers#writeTo(Object, Class, Type, Annotation[], MediaType, + * MultivaluedMap, PropertiesDelegate, OutputStream, Iterable)} throws an {@link IOException}. + * This state is always regarded as connection failure. + */ + /* package */ void doWriteEntity(final MessageBodyWorkers writeWorkers, final GenericType<?> entityType) throws IOException { + OutputStream entityStream = null; + boolean connectionFailed = false; + boolean runtimeException = false; + try { + try { + entityStream = writeWorkers.writeTo( + getEntity(), + entityType.getRawType(), + entityType.getType(), + getEntityAnnotations(), + getMediaType(), + getHeaders(), + getPropertiesDelegate(), + getEntityStream(), + writerInterceptors); + setEntityStream(entityStream); + } catch (final IOException e) { + // JERSEY-2728 - treat SSLException as connection failure + connectionFailed = true; + throw e; + } catch (final RuntimeException e) { + runtimeException = true; + throw e; + } + } finally { + // in case we've seen the ConnectException, we won't try to close/commit stream as this would produce just + // another instance of ConnectException (which would be logged even if the previously thrown one is propagated) + // However, if another failure occurred, we still have to try to close and commit the stream - and if we experience + // another failure, there is a valid reason to log it + if (!connectionFailed) { + if (entityStream != null) { + try { + entityStream.close(); + } catch (final IOException e) { + ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER, + LocalizationMessages.ERROR_CLOSING_OUTPUT_STREAM(), Level.FINE); + } catch (final RuntimeException e) { + ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER, + LocalizationMessages.ERROR_CLOSING_OUTPUT_STREAM(), Level.FINE); + } + } + try { + commitStream(); + } catch (final IOException e) { + ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER, + LocalizationMessages.ERROR_COMMITTING_OUTPUT_STREAM(), Level.FINE); + } catch (final RuntimeException e) { + ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER, + LocalizationMessages.ERROR_COMMITTING_OUTPUT_STREAM(), Level.FINE); + } + } + } + } + + private void ensureMediaType() { + if (getMediaType() == null) { + // Content-Type is not present choose a default type + final GenericType<?> entityType = new GenericType(getEntityType()); + final List<MediaType> mediaTypes = workers.getMessageBodyWriterMediaTypes( + entityType.getRawType(), entityType.getType(), getEntityAnnotations()); + + setMediaType(getMediaType(mediaTypes)); + } + } + + private MediaType getMediaType(final List<MediaType> mediaTypes) { + if (mediaTypes.isEmpty()) { + return MediaType.APPLICATION_OCTET_STREAM_TYPE; + } else { + MediaType mediaType = mediaTypes.get(0); + if (mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { + mediaType = MediaType.APPLICATION_OCTET_STREAM_TYPE; + } + return mediaType; + } + } + + /** + * Set writer interceptors for this request. + * @param writerInterceptors Writer interceptors in the interceptor execution order. + */ + void setWriterInterceptors(final Iterable<WriterInterceptor> writerInterceptors) { + this.writerInterceptors = writerInterceptors; + } + + /** + * Get writer interceptors of this request. + * @return Writer interceptors in the interceptor execution order. + */ + public Iterable<WriterInterceptor> getWriterInterceptors() { + return writerInterceptors; + } + + /** + * Get reader interceptors of this request. + * @return Reader interceptors in the interceptor execution order. + */ + public Iterable<ReaderInterceptor> getReaderInterceptors() { + return readerInterceptors; + } + + /** + * Set reader interceptors for this request. + * @param readerInterceptors Reader interceptors in the interceptor execution order. + */ + void setReaderInterceptors(final Iterable<ReaderInterceptor> readerInterceptors) { + this.readerInterceptors = readerInterceptors; + } + + @Override + public InjectionManager getInjectionManager() { + return getClientRuntime().getInjectionManager(); + } + + /** + * Indicates whether the User-Agent header should be omitted if not directly set to the map of headers. + * + * @return {@code true} if the header should be omitted, {@code false} otherwise. + */ + public boolean ignoreUserAgent() { + return ignoreUserAgent; + } + + /** + * Indicates whether the User-Agent header should be omitted if not directly set to the map of headers. + * + * @param ignore {@code true} if the header should be omitted, {@code false} otherwise. + */ + public void ignoreUserAgent(final boolean ignore) { + this.ignoreUserAgent = ignore; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java new file mode 100644 index 0000000..93301c2 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
@@ -0,0 +1,437 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.WriterInterceptor; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManagerSupplier; +import org.glassfish.jersey.message.internal.InboundMessageContext; +import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; +import org.glassfish.jersey.message.internal.Statuses; + +/** + * Jersey client response context. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ClientResponse extends InboundMessageContext implements ClientResponseContext, InjectionManagerSupplier { + + private Response.StatusType status; + private final ClientRequest requestContext; + private URI resolvedUri; + + /** + * Create new Jersey client response context initialized from a JAX-RS {@link Response response}. + * + * @param requestContext associated request context. + * @param response JAX-RS response to be used to initialize the response context. + */ + public ClientResponse(final ClientRequest requestContext, final Response response) { + this(response.getStatusInfo(), requestContext); + this.headers(OutboundJaxrsResponse.from(response).getContext().getStringHeaders()); + + final Object entity = response.getEntity(); + if (entity != null) { + InputStream entityStream = new InputStream() { + + private ByteArrayInputStream byteArrayInputStream = null; + + @Override + public int read() throws IOException { + if (byteArrayInputStream == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStream stream = null; + try { + try { + stream = requestContext.getWorkers().writeTo( + entity, entity.getClass(), null, null, response.getMediaType(), + response.getMetadata(), requestContext.getPropertiesDelegate(), baos, + Collections.<WriterInterceptor>emptyList()); + } finally { + if (stream != null) { + stream.close(); + } + } + } catch (IOException e) { + // ignore + } + + byteArrayInputStream = new ByteArrayInputStream(baos.toByteArray()); + } + + return byteArrayInputStream.read(); + } + }; + setEntityStream(entityStream); + } + } + + /** + * Create a new Jersey client response context. + * + * @param status response status. + * @param requestContext associated client request context. + */ + public ClientResponse(Response.StatusType status, ClientRequest requestContext) { + this(status, requestContext, requestContext.getUri()); + } + + /** + * Create a new Jersey client response context. + * + * @param status response status. + * @param requestContext associated client request context. + * @param resolvedRequestUri resolved request URI (see {@link #getResolvedRequestUri()}). + */ + public ClientResponse(Response.StatusType status, ClientRequest requestContext, URI resolvedRequestUri) { + this.status = status; + this.resolvedUri = resolvedRequestUri; + this.requestContext = requestContext; + + setWorkers(requestContext.getWorkers()); + } + + @Override + public int getStatus() { + return status.getStatusCode(); + } + + @Override + public void setStatus(int code) { + this.status = Statuses.from(code); + } + + @Override + public void setStatusInfo(Response.StatusType status) { + if (status == null) { + throw new NullPointerException(LocalizationMessages.CLIENT_RESPONSE_STATUS_NULL()); + } + this.status = status; + } + + @Override + public Response.StatusType getStatusInfo() { + return status; + } + + /** + * Get the absolute URI of the ultimate request made to receive this response. + * <p> + * The returned URI points to the ultimate location of the requested resource that + * provided the data represented by this response instance. Because Jersey client connectors + * may be configured to {@link ClientProperties#FOLLOW_REDIRECTS + * automatically follow redirect responses}, the value of the URI returned by this method may + * be different from the value of the {@link javax.ws.rs.client.ClientRequestContext#getUri() + * original request URI} that can be retrieved using {@code response.getRequestContext().getUri()} + * chain of method calls. + * </p> + * + * @return absolute URI of the ultimate request made to receive this response. + * + * @see ClientProperties#FOLLOW_REDIRECTS + * @see #setResolvedRequestUri(java.net.URI) + * @since 2.6 + */ + public URI getResolvedRequestUri() { + return resolvedUri; + } + + /** + * Set the absolute URI of the ultimate request that was made to receive this response. + * <p> + * If the original request URI has been modified (e.g. due to redirections), the absolute URI of + * the ultimate request being made to receive the response should be set by the caller + * on the response instance using this method. + * </p> + * + * @param uri absolute URI of the ultimate request made to receive this response. Must not be {@code null}. + * @throws java.lang.NullPointerException in case the passed {@code uri} parameter is null. + * @throws java.lang.IllegalArgumentException in case the passed {@code uri} parameter does + * not represent an absolute URI. + * @see ClientProperties#FOLLOW_REDIRECTS + * @see #getResolvedRequestUri() + * @since 2.6 + */ + public void setResolvedRequestUri(final URI uri) { + if (uri == null) { + throw new NullPointerException(LocalizationMessages.CLIENT_RESPONSE_RESOLVED_URI_NULL()); + } + if (!uri.isAbsolute()) { + throw new IllegalArgumentException(LocalizationMessages.CLIENT_RESPONSE_RESOLVED_URI_NOT_ABSOLUTE()); + } + this.resolvedUri = uri; + } + + /** + * Get the associated client request context paired with this response context. + * + * @return associated client request context. + */ + public ClientRequest getRequestContext() { + return requestContext; + } + + @Override + public Map<String, NewCookie> getCookies() { + return super.getResponseCookies(); + } + + @Override + public Set<Link> getLinks() { + return super.getLinks() + .stream() + .map(link -> { + if (link.getUri().isAbsolute()) { + return link; + } + + return Link.fromLink(link).baseUri(getResolvedRequestUri()).build(); + }) + .collect(Collectors.toSet()); + } + + @Override + public String toString() { + return "ClientResponse{" + + "method=" + requestContext.getMethod() + + ", uri=" + requestContext.getUri() + + ", status=" + status.getStatusCode() + + ", reason=" + status.getReasonPhrase() + + "}"; + } + + /** + * Get the message entity Java instance. Returns {@code null} if the message + * does not contain an entity body. + * <p> + * If the entity is represented by an un-consumed {@link InputStream input stream} + * the method will return the input stream. + * </p> + * + * @return the message entity or {@code null} if message does not contain an + * entity body (i.e. when {@link #hasEntity()} returns {@code false}). + * @throws IllegalStateException if the entity was previously fully consumed + * as an {@link InputStream input stream}, or + * if the response has been {@link #close() closed}. + * @see javax.ws.rs.core.Response#getEntity() + * @since 2.5 + */ + public Object getEntity() throws IllegalStateException { + // TODO implement some advanced caching support? + return getEntityStream(); + } + + /** + * Read the message entity input stream as an instance of specified Java type + * using a {@link javax.ws.rs.ext.MessageBodyReader} that supports mapping the + * message entity stream onto the requested type. + * <p> + * Method throws an {@link ProcessingException} if the content of the + * message cannot be mapped to an entity of the requested type and + * {@link IllegalStateException} in case the entity is not backed by an input + * stream or if the original entity input stream has already been consumed + * without {@link #bufferEntity() buffering} the entity data prior consuming. + * </p> + * <p> + * A message instance returned from this method will be cached for + * subsequent retrievals via {@link #getEntity()}. Unless the supplied entity + * type is an {@link java.io.InputStream input stream}, this method automatically + * {@link #close() closes} the an unconsumed original response entity data stream + * if open. In case the entity data has been buffered, the buffer will be reset + * prior consuming the buffered data to enable subsequent invocations of + * {@code readEntity(...)} methods on this response. + * </p> + * + * @param <T> entity instance Java type. + * @param entityType the type of entity. + * @return the message entity; for a zero-length response entities returns a corresponding + * Java object that represents zero-length data. In case no zero-length representation + * is defined for the Java type, a {@link ProcessingException} wrapping the + * underlying {@link javax.ws.rs.core.NoContentException} is thrown. + * @throws ProcessingException if the content of the message cannot be + * mapped to an entity of the requested type. + * @throws IllegalStateException if the entity is not backed by an input stream, + * the response has been {@link #close() closed} already, + * or if the entity input stream has been fully consumed already and has + * not been buffered prior consuming. + * @see javax.ws.rs.ext.MessageBodyReader + * @see javax.ws.rs.core.Response#readEntity(Class) + * @since 2.5 + */ + public <T> T readEntity(Class<T> entityType) throws ProcessingException, IllegalStateException { + return readEntity(entityType, requestContext.getPropertiesDelegate()); + } + + /** + * Read the message entity input stream as an instance of specified Java type + * using a {@link javax.ws.rs.ext.MessageBodyReader} that supports mapping the + * message entity stream onto the requested type. + * <p> + * Method throws an {@link ProcessingException} if the content of the + * message cannot be mapped to an entity of the requested type and + * {@link IllegalStateException} in case the entity is not backed by an input + * stream or if the original entity input stream has already been consumed + * without {@link #bufferEntity() buffering} the entity data prior consuming. + * </p> + * <p> + * A message instance returned from this method will be cached for + * subsequent retrievals via {@link #getEntity()}. Unless the supplied entity + * type is an {@link java.io.InputStream input stream}, this method automatically + * {@link #close() closes} the an unconsumed original response entity data stream + * if open. In case the entity data has been buffered, the buffer will be reset + * prior consuming the buffered data to enable subsequent invocations of + * {@code readEntity(...)} methods on this response. + * </p> + * + * @param <T> entity instance Java type. + * @param entityType the type of entity; may be generic. + * @return the message entity; for a zero-length response entities returns a corresponding + * Java object that represents zero-length data. In case no zero-length representation + * is defined for the Java type, a {@link ProcessingException} wrapping the + * underlying {@link javax.ws.rs.core.NoContentException} is thrown. + * @throws ProcessingException if the content of the message cannot be + * mapped to an entity of the requested type. + * @throws IllegalStateException if the entity is not backed by an input stream, + * the response has been {@link #close() closed} already, + * or if the entity input stream has been fully consumed already and has + * not been buffered prior consuming. + * @see javax.ws.rs.ext.MessageBodyReader + * @see javax.ws.rs.core.Response#readEntity(javax.ws.rs.core.GenericType) + * @since 2.5 + */ + @SuppressWarnings("unchecked") + public <T> T readEntity(GenericType<T> entityType) throws ProcessingException, IllegalStateException { + return (T) readEntity(entityType.getRawType(), entityType.getType(), requestContext.getPropertiesDelegate()); + } + + /** + * Read the message entity input stream as an instance of specified Java type + * using a {@link javax.ws.rs.ext.MessageBodyReader} that supports mapping the + * message entity stream onto the requested type. + * <p> + * Method throws an {@link ProcessingException} if the content of the + * message cannot be mapped to an entity of the requested type and + * {@link IllegalStateException} in case the entity is not backed by an input + * stream or if the original entity input stream has already been consumed + * without {@link #bufferEntity() buffering} the entity data prior consuming. + * </p> + * <p> + * A message instance returned from this method will be cached for + * subsequent retrievals via {@link #getEntity()}. Unless the supplied entity + * type is an {@link java.io.InputStream input stream}, this method automatically + * {@link #close() closes} the an unconsumed original response entity data stream + * if open. In case the entity data has been buffered, the buffer will be reset + * prior consuming the buffered data to enable subsequent invocations of + * {@code readEntity(...)} methods on this response. + * </p> + * + * @param <T> entity instance Java type. + * @param entityType the type of entity. + * @param annotations annotations that will be passed to the {@link javax.ws.rs.ext.MessageBodyReader}. + * @return the message entity; for a zero-length response entities returns a corresponding + * Java object that represents zero-length data. In case no zero-length representation + * is defined for the Java type, a {@link ProcessingException} wrapping the + * underlying {@link javax.ws.rs.core.NoContentException} is thrown. + * @throws ProcessingException if the content of the message cannot be + * mapped to an entity of the requested type. + * @throws IllegalStateException if the entity is not backed by an input stream, + * the response has been {@link #close() closed} already, + * or if the entity input stream has been fully consumed already and has + * not been buffered prior consuming. + * @see javax.ws.rs.ext.MessageBodyReader + * @see javax.ws.rs.core.Response#readEntity(Class, java.lang.annotation.Annotation[]) + * @since 2.5 + */ + public <T> T readEntity(Class<T> entityType, Annotation[] annotations) throws ProcessingException, IllegalStateException { + return readEntity(entityType, annotations, requestContext.getPropertiesDelegate()); + } + + /** + * Read the message entity input stream as an instance of specified Java type + * using a {@link javax.ws.rs.ext.MessageBodyReader} that supports mapping the + * message entity stream onto the requested type. + * <p> + * Method throws an {@link ProcessingException} if the content of the + * message cannot be mapped to an entity of the requested type and + * {@link IllegalStateException} in case the entity is not backed by an input + * stream or if the original entity input stream has already been consumed + * without {@link #bufferEntity() buffering} the entity data prior consuming. + * </p> + * <p> + * A message instance returned from this method will be cached for + * subsequent retrievals via {@link #getEntity()}. Unless the supplied entity + * type is an {@link java.io.InputStream input stream}, this method automatically + * {@link #close() closes} the an unconsumed original response entity data stream + * if open. In case the entity data has been buffered, the buffer will be reset + * prior consuming the buffered data to enable subsequent invocations of + * {@code readEntity(...)} methods on this response. + * </p> + * + * @param <T> entity instance Java type. + * @param entityType the type of entity; may be generic. + * @param annotations annotations that will be passed to the {@link javax.ws.rs.ext.MessageBodyReader}. + * @return the message entity; for a zero-length response entities returns a corresponding + * Java object that represents zero-length data. In case no zero-length representation + * is defined for the Java type, a {@link ProcessingException} wrapping the + * underlying {@link javax.ws.rs.core.NoContentException} is thrown. + * @throws ProcessingException if the content of the message cannot be + * mapped to an entity of the requested type. + * @throws IllegalStateException if the entity is not backed by an input stream, + * the response has been {@link #close() closed} already, + * or if the entity input stream has been fully consumed already and has + * not been buffered prior consuming. + * @see javax.ws.rs.ext.MessageBodyReader + * @see javax.ws.rs.core.Response#readEntity(javax.ws.rs.core.GenericType, java.lang.annotation.Annotation[]) + * @since 2.5 + */ + @SuppressWarnings("unchecked") + public <T> T readEntity(GenericType<T> entityType, Annotation[] annotations) + throws ProcessingException, IllegalStateException { + return (T) readEntity(entityType.getRawType(), entityType.getType(), annotations, requestContext.getPropertiesDelegate()); + } + + @Override + public InjectionManager getInjectionManager() { + return getRequestContext().getInjectionManager(); + } + + @Override + protected Iterable<ReaderInterceptor> getReaderInterceptors() { + return requestContext.getReaderInterceptors(); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java new file mode 100644 index 0000000..e40da77 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java
@@ -0,0 +1,353 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; + +import javax.inject.Provider; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.internal.BootstrapBag; +import org.glassfish.jersey.internal.Version; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.message.MessageBodyWorkers; +import org.glassfish.jersey.model.internal.ManagedObjectsFinalizer; +import org.glassfish.jersey.process.internal.ChainableStage; +import org.glassfish.jersey.process.internal.RequestContext; +import org.glassfish.jersey.process.internal.RequestScope; +import org.glassfish.jersey.process.internal.Stage; +import org.glassfish.jersey.process.internal.Stages; + +/** + * Client-side request processing runtime. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +class ClientRuntime implements JerseyClient.ShutdownHook, ClientExecutor { + + private static final Logger LOG = Logger.getLogger(ClientRuntime.class.getName()); + + private final Stage<ClientRequest> requestProcessingRoot; + private final Stage<ClientResponse> responseProcessingRoot; + + private final Connector connector; + private final ClientConfig config; + + private final RequestScope requestScope; + private final LazyValue<ExecutorService> asyncRequestExecutor; + private final LazyValue<ScheduledExecutorService> backgroundScheduler; + + private final Iterable<ClientLifecycleListener> lifecycleListeners; + + private final AtomicBoolean closed = new AtomicBoolean(false); + private final ManagedObjectsFinalizer managedObjectsFinalizer; + private final InjectionManager injectionManager; + + /** + * Create new client request processing runtime. + * + * @param config client runtime configuration. + * @param connector client transport connector. + * @param injectionManager injection manager. + */ + public ClientRuntime(final ClientConfig config, final Connector connector, final InjectionManager injectionManager, + final BootstrapBag bootstrapBag) { + Provider<Ref<ClientRequest>> clientRequest = + () -> injectionManager.getInstance(new GenericType<Ref<ClientRequest>>() {}.getType()); + + RequestProcessingInitializationStage requestProcessingInitializationStage = + new RequestProcessingInitializationStage(clientRequest, bootstrapBag.getMessageBodyWorkers(), injectionManager); + + Stage.Builder<ClientRequest> requestingChainBuilder = Stages.chain(requestProcessingInitializationStage); + + ChainableStage<ClientRequest> requestFilteringStage = ClientFilteringStages.createRequestFilteringStage(injectionManager); + this.requestProcessingRoot = requestFilteringStage != null + ? requestingChainBuilder.build(requestFilteringStage) : requestingChainBuilder.build(); + + ChainableStage<ClientResponse> responseFilteringStage = ClientFilteringStages.createResponseFilteringStage( + injectionManager); + this.responseProcessingRoot = responseFilteringStage != null ? responseFilteringStage : Stages.identity(); + this.managedObjectsFinalizer = bootstrapBag.getManagedObjectsFinalizer(); + this.config = config; + this.connector = connector; + this.requestScope = bootstrapBag.getRequestScope(); + this.asyncRequestExecutor = Values.lazy((Value<ExecutorService>) () -> + config.getExecutorService() == null + ? injectionManager.getInstance(ExecutorService.class, ClientAsyncExecutorLiteral.INSTANCE) + : config.getExecutorService()); + this.backgroundScheduler = Values.lazy((Value<ScheduledExecutorService>) () -> + config.getScheduledExecutorService() == null + ? injectionManager.getInstance(ScheduledExecutorService.class, ClientBackgroundSchedulerLiteral.INSTANCE) + : config.getScheduledExecutorService()); + + this.injectionManager = injectionManager; + this.lifecycleListeners = Providers.getAllProviders(injectionManager, ClientLifecycleListener.class); + + for (final ClientLifecycleListener listener : lifecycleListeners) { + try { + listener.onInit(); + } catch (final Throwable t) { + LOG.log(Level.WARNING, LocalizationMessages.ERROR_LISTENER_INIT(listener.getClass().getName()), t); + } + } + } + + /** + * Prepare a {@code Runnable} to be used to submit a {@link ClientRequest client request} for asynchronous processing. + * <p> + * + * @param request client request to be sent. + * @param callback asynchronous response callback. + * @return {@code Runnable} to be submitted for async processing using {@link #submit(Runnable)}. + */ + Runnable createRunnableForAsyncProcessing(ClientRequest request, final ResponseCallback callback) { + return () -> requestScope.runInScope(() -> { + try { + ClientRequest processedRequest; + try { + processedRequest = Stages.process(request, requestProcessingRoot); + processedRequest = addUserAgent(processedRequest, connector.getName()); + } catch (final AbortException aborted) { + processResponse(aborted.getAbortResponse(), callback); + return; + } + + final AsyncConnectorCallback connectorCallback = new AsyncConnectorCallback() { + + @Override + public void response(final ClientResponse response) { + requestScope.runInScope(() -> processResponse(response, callback)); + } + + @Override + public void failure(final Throwable failure) { + requestScope.runInScope(() -> processFailure(failure, callback)); + } + }; + + connector.apply(processedRequest, connectorCallback); + } catch (final Throwable throwable) { + processFailure(throwable, callback); + } + }); + } + + @Override + public <T> Future<T> submit(Callable<T> task) { + return asyncRequestExecutor.get().submit(task); + } + + @Override + public Future<?> submit(Runnable task) { + return asyncRequestExecutor.get().submit(task); + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + return asyncRequestExecutor.get().submit(task, result); + } + + @Override + public <T> ScheduledFuture<T> schedule(Callable<T> callable, long delay, TimeUnit unit) { + return backgroundScheduler.get().schedule(callable, delay, unit); + } + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + return backgroundScheduler.get().schedule(command, delay, unit); + } + + private void processResponse(final ClientResponse response, final ResponseCallback callback) { + final ClientResponse processedResponse; + try { + processedResponse = Stages.process(response, responseProcessingRoot); + } catch (final Throwable throwable) { + processFailure(throwable, callback); + return; + } + callback.completed(processedResponse, requestScope); + } + + private void processFailure(final Throwable failure, final ResponseCallback callback) { + callback.failed(failure instanceof ProcessingException + ? (ProcessingException) failure : new ProcessingException(failure)); + } + + private Future<?> submit(final ExecutorService executor, final Runnable task) { + return executor.submit(() -> requestScope.runInScope(task)); + } + + private ClientRequest addUserAgent(final ClientRequest clientRequest, final String connectorName) { + final MultivaluedMap<String, Object> headers = clientRequest.getHeaders(); + + if (headers.containsKey(HttpHeaders.USER_AGENT)) { + // Check for explicitly set null value and if set, then remove the header - see JERSEY-2189 + if (clientRequest.getHeaderString(HttpHeaders.USER_AGENT) == null) { + headers.remove(HttpHeaders.USER_AGENT); + } + } else if (!clientRequest.ignoreUserAgent()) { + if (connectorName != null && !connectorName.isEmpty()) { + headers.put(HttpHeaders.USER_AGENT, + Collections.singletonList(String.format("Jersey/%s (%s)", Version.getVersion(), connectorName))); + } else { + headers.put(HttpHeaders.USER_AGENT, + Collections.singletonList(String.format("Jersey/%s", Version.getVersion()))); + } + } + + return clientRequest; + } + + /** + * Invoke a request processing synchronously in the context of the caller's thread. + * <p> + * NOTE: the method does not explicitly start a new request scope context. Instead + * it is assumed that the method is invoked from within a context of a proper, running + * {@link RequestContext request context}. A caller may use the + * {@link #getRequestScope()} method to retrieve the request scope instance and use it to + * initialize the proper request scope context prior the method invocation. + * </p> + * + * @param request client request to be invoked. + * @return client response. + * @throws javax.ws.rs.ProcessingException in case of an invocation failure. + */ + public ClientResponse invoke(final ClientRequest request) { + ClientResponse response; + try { + try { + response = connector.apply(addUserAgent(Stages.process(request, requestProcessingRoot), connector.getName())); + } catch (final AbortException aborted) { + response = aborted.getAbortResponse(); + } + + return Stages.process(response, responseProcessingRoot); + } catch (final ProcessingException pe) { + throw pe; + } catch (final Throwable t) { + throw new ProcessingException(t.getMessage(), t); + } + } + + /** + * Get the request scope instance configured for the runtime. + * + * @return request scope instance. + */ + public RequestScope getRequestScope() { + return requestScope; + } + + /** + * Get runtime configuration. + * + * @return runtime configuration. + */ + public ClientConfig getConfig() { + return config; + } + + /** + * This will be used as the last resort to clean things up + * in the case that this instance gets garbage collected + * before the client itself gets released. + * <p> + * Close will be invoked either via finalizer + * or via JerseyClient onShutdown hook, whatever comes first. + */ + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + @Override + public void onShutdown() { + close(); + } + + private void close() { + if (closed.compareAndSet(false, true)) { + try { + for (final ClientLifecycleListener listener : lifecycleListeners) { + try { + listener.onClose(); + } catch (final Throwable t) { + LOG.log(Level.WARNING, LocalizationMessages.ERROR_LISTENER_CLOSE(listener.getClass().getName()), t); + } + } + } finally { + try { + connector.close(); + } finally { + managedObjectsFinalizer.preDestroy(); + injectionManager.shutdown(); + } + } + } + } + + /** + * Pre-initialize the client runtime. + */ + public void preInitialize() { + // pre-initialize MessageBodyWorkers + injectionManager.getInstance(MessageBodyWorkers.class); + } + + /** + * Runtime connector. + * + * @return runtime connector. + */ + public Connector getConnector() { + return connector; + } + + /** + * Get injection manager. + * + * @return injection manager. + */ + InjectionManager getInjectionManager() { + return injectionManager; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/CustomProvidersFeature.java b/core-client/src/main/java/org/glassfish/jersey/client/CustomProvidersFeature.java new file mode 100644 index 0000000..f2c255b --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/CustomProvidersFeature.java
@@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Collection; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +/** + * Feature to provide the single-line registration of custom providers. + * + * @author Stepan Kopriva + */ +public class CustomProvidersFeature implements Feature { + + private final Collection<Class<?>> providers; + + /** + * Constructs Feature which is used to register providers as providers in Configuration. + * + * @param providers collection of providers which are going to be registered + */ + public CustomProvidersFeature(Collection<Class<?>> providers) { + this.providers = providers; + } + + @Override + public boolean configure(FeatureContext context) { + for (Class<?> provider : providers) { + context.register(provider); + } + return true; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java new file mode 100644 index 0000000..369603a --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java
@@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.logging.Logger; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.spi.ThreadPoolExecutorProvider; + +/** + * Default {@link org.glassfish.jersey.spi.ExecutorServiceProvider} used on the client side for asynchronous request processing. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@ClientAsyncExecutor +class DefaultClientAsyncExecutorProvider extends ThreadPoolExecutorProvider { + + private static final Logger LOGGER = Logger.getLogger(DefaultClientAsyncExecutorProvider.class.getName()); + + private final LazyValue<Integer> asyncThreadPoolSize; + + /** + * Creates a new instance. + * + * @param poolSize size of the default executor thread pool (if used). Zero or negative values are ignored. + * See also {@link org.glassfish.jersey.client.ClientProperties#ASYNC_THREADPOOL_SIZE}. + */ + @Inject + public DefaultClientAsyncExecutorProvider(@Named("ClientAsyncThreadPoolSize") final int poolSize) { + super("jersey-client-async-executor"); + + this.asyncThreadPoolSize = Values.lazy(new Value<Integer>() { + @Override + public Integer get() { + if (poolSize <= 0) { + LOGGER.config(LocalizationMessages.IGNORED_ASYNC_THREADPOOL_SIZE(poolSize)); + // using default + return Integer.MAX_VALUE; + } else { + LOGGER.config(LocalizationMessages.USING_FIXED_ASYNC_THREADPOOL(poolSize)); + return poolSize; + } + } + }); + } + + @Override + protected int getMaximumPoolSize() { + return asyncThreadPoolSize.get(); + } + + @Override + protected int getCorePoolSize() { + // Mimicking the Executors.newCachedThreadPool and newFixedThreadPool configuration values. + final Integer maximumPoolSize = getMaximumPoolSize(); + if (maximumPoolSize != Integer.MAX_VALUE) { + return maximumPoolSize; + } else { + return 0; + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientBackgroundSchedulerProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientBackgroundSchedulerProvider.java new file mode 100644 index 0000000..7ff298e --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientBackgroundSchedulerProvider.java
@@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import org.glassfish.jersey.spi.ScheduledThreadPoolExecutorProvider; + +/** + * Default {@link org.glassfish.jersey.spi.ScheduledExecutorServiceProvider} used on the client side for providing the scheduled + * executor service that runs background tasks. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + * @since 2.26 + */ +@ClientBackgroundScheduler +class DefaultClientBackgroundSchedulerProvider extends ScheduledThreadPoolExecutorProvider { + + /** + * Creates a new instance. + */ + DefaultClientBackgroundSchedulerProvider() { + super("jersey-client-background-scheduler"); + } + + @Override + protected int getCorePoolSize() { + return 1; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java new file mode 100644 index 0000000..86bac84 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java
@@ -0,0 +1,308 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; +import java.util.logging.Logger; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.client.internal.HttpUrlConnector; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +/** + * Default Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} provider + * that provides connector instances which delegate HTTP requests to {@link java.net.HttpURLConnection} + * for processing. + * <p> + * The provided connector instances override default behaviour of the property + * {@link ClientProperties#REQUEST_ENTITY_PROCESSING} and use {@link RequestEntityProcessing#BUFFERED} + * request entity processing by default. + * </p> + * <p> + * Due to a bug in the chunked transport coding support of {@code HttpURLConnection} that causes + * requests to fail unpredictably, this connector provider allows to configure the provided connector + * instances to use {@code HttpURLConnection}'s fixed-length streaming mode as a workaround. This + * workaround can be enabled via {@link #useFixedLengthStreaming()} method or via + * {@link #USE_FIXED_LENGTH_STREAMING} Jersey client configuration property. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class HttpUrlConnectorProvider implements ConnectorProvider { + /** + * If {@code true}, the {@link HttpUrlConnector} (if used) will assume the content length + * from the value of {@value javax.ws.rs.core.HttpHeaders#CONTENT_LENGTH} request + * header (if present). + * <p> + * When this property is enabled and the request has a valid non-zero content length + * value specified in its {@value javax.ws.rs.core.HttpHeaders#CONTENT_LENGTH} request + * header, that this value will be used as an input to the + * {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int)} method call + * invoked on the underlying {@link java.net.HttpURLConnection connection}. + * This will also suppress the entity buffering in the @{code HttpURLConnection}, + * which is undesirable in certain scenarios, e.g. when streaming large entities. + * </p> + * <p> + * Note that the content length value defined in the request header must exactly match + * the real size of the entity. If the {@link javax.ws.rs.core.HttpHeaders#CONTENT_LENGTH} header + * is explicitly specified in a request, this property will be ignored and the + * request entity will be still buffered by the underlying @{code HttpURLConnection} infrastructure. + * </p> + * <p> + * This property also overrides the behaviour enabled by the + * {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property. + * Chunked encoding will only be used, if the size is not specified in the header of the request. + * </p> + * <p> + * Note that this property only applies to client run-times that are configured to use the default + * {@link HttpUrlConnector} as the client connector. The property is ignored by other connectors. + * </p> + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @since 2.5 + */ + public static final String USE_FIXED_LENGTH_STREAMING = + "jersey.config.client.httpUrlConnector.useFixedLengthStreaming"; + + /** + * A value of {@code true} declares that the client will try to set + * unsupported HTTP method to {@link java.net.HttpURLConnection} via + * reflection. + * <p> + * NOTE: Enabling this property may cause security related warnings/errors + * and it may break when other JDK implementation is used. <b>Use only + * when you know what you are doing.</b> + * </p> + * <p>The value MUST be an instance of {@link java.lang.Boolean}.</p> + * <p>The default value is {@code false}.</p> + * <p>The name of the configuration property is <tt>{@value}</tt>.</p> + */ + public static final String SET_METHOD_WORKAROUND = + "jersey.config.client.httpUrlConnection.setMethodWorkaround"; + /** + * Default connection factory to be used. + */ + private static final ConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultConnectionFactory(); + + private static final Logger LOGGER = Logger.getLogger(HttpUrlConnectorProvider.class.getName()); + + private ConnectionFactory connectionFactory; + private int chunkSize; + private boolean useFixedLengthStreaming; + private boolean useSetMethodWorkaround; + + /** + * Create new {@link java.net.HttpURLConnection}-based Jersey client connector provider. + */ + public HttpUrlConnectorProvider() { + this.connectionFactory = DEFAULT_CONNECTION_FACTORY; + this.chunkSize = ClientProperties.DEFAULT_CHUNK_SIZE; + this.useFixedLengthStreaming = false; + this.useSetMethodWorkaround = false; + } + + /** + * Set a custom {@link java.net.HttpURLConnection} factory. + * + * @param connectionFactory custom HTTP URL connection factory. Must not be {@code null}. + * @return updated connector provider instance. + * @throws java.lang.NullPointerException in case the supplied connectionFactory is {@code null}. + */ + public HttpUrlConnectorProvider connectionFactory(final ConnectionFactory connectionFactory) { + if (connectionFactory == null) { + throw new NullPointerException(LocalizationMessages.NULL_INPUT_PARAMETER("connectionFactory")); + } + + this.connectionFactory = connectionFactory; + return this; + } + + /** + * Set chunk size for requests transferred using a + * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">HTTP chunked transfer coding</a>. + * + * If no value is set, the default chunk size of 4096 bytes will be used. + * <p> + * Note that this programmatically set value can be overridden by + * setting the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property + * specified in the Jersey client instance configuration. + * </p> + * + * @param chunkSize chunked transfer coding chunk size to be used. + * @return updated connector provider instance. + * @throws java.lang.IllegalArgumentException in case the specified chunk size is negative. + */ + public HttpUrlConnectorProvider chunkSize(final int chunkSize) { + if (chunkSize < 0) { + throw new IllegalArgumentException(LocalizationMessages.NEGATIVE_INPUT_PARAMETER("chunkSize")); + } + this.chunkSize = chunkSize; + return this; + } + + /** + * Instruct the provided connectors to use the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) + * fixed-length streaming mode} on the underlying HTTP URL connection instance when sending requests. + * See {@link #USE_FIXED_LENGTH_STREAMING} property documentation for more details. + * <p> + * Note that this programmatically set value can be overridden by + * setting the {@code USE_FIXED_LENGTH_STREAMING} property specified in the Jersey client instance configuration. + * </p> + * + * @return updated connector provider instance. + */ + public HttpUrlConnectorProvider useFixedLengthStreaming() { + this.useFixedLengthStreaming = true; + return this; + } + + /** + * Instruct the provided connectors to use reflection when setting the + * HTTP method value.See {@link #SET_METHOD_WORKAROUND} property documentation for more details. + * <p> + * Note that this programmatically set value can be overridden by + * setting the {@code SET_METHOD_WORKAROUND} property specified in the Jersey client instance configuration + * or in the request properties. + * </p> + * + * @return updated connector provider instance. + */ + public HttpUrlConnectorProvider useSetMethodWorkaround() { + this.useSetMethodWorkaround = true; + return this; + } + + @Override + public Connector getConnector(final Client client, final Configuration config) { + final Map<String, Object> properties = config.getProperties(); + + int computedChunkSize = ClientProperties.getValue(properties, + ClientProperties.CHUNKED_ENCODING_SIZE, chunkSize, Integer.class); + if (computedChunkSize < 0) { + LOGGER.warning(LocalizationMessages.NEGATIVE_CHUNK_SIZE(computedChunkSize, chunkSize)); + computedChunkSize = chunkSize; + } + + final boolean computedUseFixedLengthStreaming = ClientProperties.getValue(properties, + USE_FIXED_LENGTH_STREAMING, useFixedLengthStreaming, Boolean.class); + final boolean computedUseSetMethodWorkaround = ClientProperties.getValue(properties, + SET_METHOD_WORKAROUND, useSetMethodWorkaround, Boolean.class); + + return createHttpUrlConnector(client, connectionFactory, computedChunkSize, computedUseFixedLengthStreaming, + computedUseSetMethodWorkaround); + } + + /** + * Create {@link HttpUrlConnector}. + * + * @param client JAX-RS client instance for which the connector is being created. + * @param connectionFactory {@link javax.net.ssl.HttpsURLConnection} factory to be used when creating + * connections. + * @param chunkSize chunk size to use when using HTTP chunked transfer coding. + * @param fixLengthStreaming specify if the the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) + * fixed-length streaming mode} on the underlying HTTP URL connection instances should + * be used when sending requests. + * @param setMethodWorkaround specify if the reflection workaround should be used to set HTTP URL connection method + * name. See {@link HttpUrlConnectorProvider#SET_METHOD_WORKAROUND} for details. + * @return created {@link HttpUrlConnector} instance. + */ + protected Connector createHttpUrlConnector(Client client, ConnectionFactory connectionFactory, + int chunkSize, boolean fixLengthStreaming, + boolean setMethodWorkaround) { + return new HttpUrlConnector( + client, + connectionFactory, + chunkSize, + fixLengthStreaming, + setMethodWorkaround); + } + + /** + * A factory for {@link java.net.HttpURLConnection} instances. + * <p> + * A factory may be used to create a {@link java.net.HttpURLConnection} and configure + * it in a custom manner that is not possible using the Client API. + * <p> + * A custom factory instance may be registered in the {@code HttpUrlConnectorProvider} instance + * via {@link #connectionFactory(ConnectionFactory)} method. + */ + public interface ConnectionFactory { + + /** + * Get a {@link java.net.HttpURLConnection} for a given URL. + * <p> + * Implementation of the method MUST be thread-safe and MUST ensure that + * a dedicated {@link java.net.HttpURLConnection} instance is returned for concurrent + * requests. + * </p> + * + * @param url the endpoint URL. + * @return the {@link java.net.HttpURLConnection}. + * @throws java.io.IOException in case the connection cannot be provided. + */ + public HttpURLConnection getConnection(URL url) throws IOException; + } + + private static class DefaultConnectionFactory implements ConnectionFactory { + + @Override + public HttpURLConnection getConnection(final URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final HttpUrlConnectorProvider that = (HttpUrlConnectorProvider) o; + + if (chunkSize != that.chunkSize) { + return false; + } + if (useFixedLengthStreaming != that.useFixedLengthStreaming) { + return false; + } + + return connectionFactory.equals(that.connectionFactory); + } + + @Override + public int hashCode() { + int result = connectionFactory.hashCode(); + result = 31 * result + chunkSize; + result = 31 * result + (useFixedLengthStreaming ? 1 : 0); + return result; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/InboundJaxrsResponse.java b/core-client/src/main/java/org/glassfish/jersey/client/InboundJaxrsResponse.java new file mode 100644 index 0000000..57181aa --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/InboundJaxrsResponse.java
@@ -0,0 +1,245 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.annotation.Annotation; +import java.net.URI; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.internal.util.Producer; +import org.glassfish.jersey.process.internal.RequestContext; +import org.glassfish.jersey.process.internal.RequestScope; + +/** + * Implementation of an inbound client-side JAX-RS {@link Response} message. + * <p> + * This response delegates method calls to the underlying + * {@link org.glassfish.jersey.client.ClientResponse client response context} and + * ensures that all request-scoped method invocations are run in the proper request scope. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +class InboundJaxrsResponse extends Response { + + private final ClientResponse context; + private final RequestScope scope; + private final RequestContext requestContext; + + /** + * Create new scoped client response. + * + * @param context jersey client response context. + * @param scope request scope instance. + */ + public InboundJaxrsResponse(final ClientResponse context, final RequestScope scope) { + this.context = context; + this.scope = scope; + if (this.scope != null) { + this.requestContext = scope.referenceCurrent(); + } else { + this.requestContext = null; + } + } + + @Override + public int getStatus() { + return context.getStatus(); + } + + @Override + public StatusType getStatusInfo() { + return context.getStatusInfo(); + } + + @Override + public Object getEntity() throws IllegalStateException { + return context.getEntity(); + } + + @Override + public <T> T readEntity(final Class<T> entityType) throws ProcessingException, IllegalStateException { + return runInScopeIfPossible(new Producer<T>() { + @Override + public T call() { + return context.readEntity(entityType); + } + }); + } + + @Override + @SuppressWarnings("unchecked") + public <T> T readEntity(final GenericType<T> entityType) throws ProcessingException, IllegalStateException { + return runInScopeIfPossible(new Producer<T>() { + @Override + public T call() { + return context.readEntity(entityType); + } + }); + } + + @Override + public <T> T readEntity(final Class<T> entityType, final Annotation[] annotations) + throws ProcessingException, IllegalStateException { + return runInScopeIfPossible(new Producer<T>() { + @Override + public T call() { + return context.readEntity(entityType, annotations); + } + }); + } + + @Override + @SuppressWarnings("unchecked") + public <T> T readEntity(final GenericType<T> entityType, final Annotation[] annotations) + throws ProcessingException, IllegalStateException { + return runInScopeIfPossible(new Producer<T>() { + @Override + public T call() { + return context.readEntity(entityType, annotations); + } + }); + } + + @Override + public boolean hasEntity() { + return context.hasEntity(); + } + + @Override + public boolean bufferEntity() throws ProcessingException { + return context.bufferEntity(); + } + + @Override + public void close() throws ProcessingException { + try { + context.close(); + } finally { + if (requestContext != null) { + requestContext.release(); + } + } + } + + @Override + public String getHeaderString(String name) { + return context.getHeaderString(name); + } + + @Override + public MultivaluedMap<String, String> getStringHeaders() { + return context.getHeaders(); + } + + @Override + public MediaType getMediaType() { + return context.getMediaType(); + } + + @Override + public Locale getLanguage() { + return context.getLanguage(); + } + + @Override + public int getLength() { + return context.getLength(); + } + + @Override + public Map<String, NewCookie> getCookies() { + return context.getResponseCookies(); + } + + @Override + public EntityTag getEntityTag() { + return context.getEntityTag(); + } + + @Override + public Date getDate() { + return context.getDate(); + } + + @Override + public Date getLastModified() { + return context.getLastModified(); + } + + @Override + public Set<String> getAllowedMethods() { + return context.getAllowedMethods(); + } + + @Override + public URI getLocation() { + return context.getLocation(); + } + + @Override + public Set<Link> getLinks() { + return context.getLinks(); + } + + @Override + public boolean hasLink(String relation) { + return context.hasLink(relation); + } + + @Override + public Link getLink(String relation) { + return context.getLink(relation); + } + + @Override + public Link.Builder getLinkBuilder(String relation) { + return context.getLinkBuilder(relation); + } + + @Override + @SuppressWarnings("unchecked") + public MultivaluedMap<String, Object> getMetadata() { + final MultivaluedMap<String, ?> headers = context.getHeaders(); + return (MultivaluedMap<String, Object>) headers; + } + + @Override + public String toString() { + return "InboundJaxrsResponse{" + "context=" + context + "}"; + } + + private <T> T runInScopeIfPossible(Producer<T> producer) { + if (scope != null && requestContext != null) { + return scope.runInScope(requestContext, producer); + } else { + return producer.call(); + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/Initializable.java b/core-client/src/main/java/org/glassfish/jersey/client/Initializable.java new file mode 100644 index 0000000..1c8f4c2 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/Initializable.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +/** + * Initializable Jersey client-side component. + * <p> + * This interface provides method that allows to pre-initialize client-side component's runtime and runtime configuration + * ahead of it's first use. The interface is implemented by {@link org.glassfish.jersey.client.JerseyClient} and + * {@link org.glassfish.jersey.client.JerseyWebTarget} classes. + * </p> + * + * @param <T> initializable type. + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.8 + */ +public interface Initializable<T extends Initializable<T>> { + + /** + * Pre-initializes the runtime and runtime {@link javax.ws.rs.core.Configuration configuration} of this component + * in order to improve performance during the first request. + * <p> + * Once this method is called no other method implementing {@link javax.ws.rs.core.Configurable} should be called + * on this pre initialized component, otherwise the initialized client runtime will be discarded and the configuration + * will change back to uninitialized. + * </p> + * + * @return pre-initialized Jersey client component. + */ + T preInitialize(); + + /** + * Get a live view of an internal client configuration state of this initializable instance. + * + * @return configuration live view of the internal configuration state. + */ + ClientConfig getConfiguration(); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/InjectionManagerClientProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/InjectionManagerClientProvider.java new file mode 100644 index 0000000..37c94bc --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/InjectionManagerClientProvider.java
@@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; + +import org.glassfish.jersey.InjectionManagerProvider; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManagerSupplier; + +/** + * Extension of {@link InjectionManagerProvider} which contains helper static methods + * that extract {@link InjectionManager} from client specific JAX-RS components. + * <p> + * See javadoc of {@link InjectionManagerProvider} for more details. + * </p> + * + * @see InjectionManagerProvider + * @author Miroslav Fuksa + * @since 2.6 + */ +public class InjectionManagerClientProvider extends InjectionManagerProvider { + + /** + * Extract and return injection manager from {@link javax.ws.rs.client.ClientRequestContext clientRequestContext}. + * The method can be used to inject custom types into a {@link javax.ws.rs.client.ClientRequestFilter}. + * + * @param clientRequestContext Client request context. + * + * @return injection manager. + * + * @throws java.lang.IllegalArgumentException when {@code clientRequestContext} is not a default + * Jersey implementation provided by Jersey as argument in the + * {@link javax.ws.rs.client.ClientRequestFilter#filter(javax.ws.rs.client.ClientRequestContext)} method. + */ + public static InjectionManager getInjectionManager(ClientRequestContext clientRequestContext) { + if (!(clientRequestContext instanceof InjectionManagerSupplier)) { + throw new IllegalArgumentException( + LocalizationMessages + .ERROR_SERVICE_LOCATOR_PROVIDER_INSTANCE_REQUEST(clientRequestContext.getClass().getName())); + } + return ((InjectionManagerSupplier) clientRequestContext).getInjectionManager(); + } + + /** + * Extract and return injection manager from {@link javax.ws.rs.client.ClientResponseContext clientResponseContext}. + * The method can be used to inject custom types into a {@link javax.ws.rs.client.ClientResponseFilter}. + * + * @param clientResponseContext Client response context. + * + * @return injection manager. + * + * @throws java.lang.IllegalArgumentException when {@code clientResponseContext} is not a default + * Jersey implementation provided by Jersey as argument in the + * {@link javax.ws.rs.client.ClientResponseFilter#filter(javax.ws.rs.client.ClientRequestContext, javax.ws.rs.client.ClientResponseContext)} + * method. + */ + public static InjectionManager getInjectionManager(ClientResponseContext clientResponseContext) { + if (!(clientResponseContext instanceof InjectionManagerSupplier)) { + throw new IllegalArgumentException( + LocalizationMessages + .ERROR_SERVICE_LOCATOR_PROVIDER_INSTANCE_RESPONSE(clientResponseContext.getClass().getName())); + } + return ((InjectionManagerSupplier) clientResponseContext).getInjectionManager(); + } + +} +
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java new file mode 100644 index 0000000..d94824d --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
@@ -0,0 +1,445 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.net.URI; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.UriBuilder; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.spi.DefaultSslContextProvider; +import org.glassfish.jersey.internal.ServiceFinder; +import org.glassfish.jersey.internal.util.collection.UnsafeValue; +import org.glassfish.jersey.internal.util.collection.Values; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; +import static org.glassfish.jersey.internal.guava.Preconditions.checkState; + +/** + * Jersey implementation of {@link javax.ws.rs.client.Client JAX-RS Client} + * contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JerseyClient implements javax.ws.rs.client.Client, Initializable<JerseyClient> { + private static final Logger LOG = Logger.getLogger(JerseyClient.class.getName()); + + private static final DefaultSslContextProvider DEFAULT_SSL_CONTEXT_PROVIDER = new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + return SslConfigurator.getDefaultContext(); + } + }; + + private final AtomicBoolean closedFlag = new AtomicBoolean(false); + private final boolean isDefaultSslContext; + private final ClientConfig config; + private final HostnameVerifier hostnameVerifier; + private final UnsafeValue<SSLContext, IllegalStateException> sslContext; + private final LinkedBlockingDeque<WeakReference<JerseyClient.ShutdownHook>> shutdownHooks = + new LinkedBlockingDeque<WeakReference<JerseyClient.ShutdownHook>>(); + private final ReferenceQueue<JerseyClient.ShutdownHook> shReferenceQueue = new ReferenceQueue<JerseyClient.ShutdownHook>(); + private final ExecutorService executorService; + private final ScheduledExecutorService scheduledExecutorService; + + /** + * Client instance shutdown hook. + */ + interface ShutdownHook { + /** + * Invoked when the client instance is closed. + */ + public void onShutdown(); + } + + /** + * Create a new Jersey client instance using a default configuration. + */ + protected JerseyClient() { + this(null, (UnsafeValue<SSLContext, IllegalStateException>) null, null, null); + } + + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContext jersey client SSL context. + * @param verifier jersey client host name verifier. + */ + protected JerseyClient(final Configuration config, + final SSLContext sslContext, + final HostnameVerifier verifier) { + + this(config, sslContext, verifier, null); + } + + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContext jersey client SSL context. + * @param verifier jersey client host name verifier. + * @param defaultSslContextProvider default SSL context provider. + */ + protected JerseyClient(final Configuration config, + final SSLContext sslContext, + final HostnameVerifier verifier, + final DefaultSslContextProvider defaultSslContextProvider) { + this(config, sslContext == null ? null : Values.unsafe(sslContext), verifier, + defaultSslContextProvider); + } + + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContextProvider jersey client SSL context provider. + * @param verifier jersey client host name verifier. + */ + protected JerseyClient(final Configuration config, + final UnsafeValue<SSLContext, IllegalStateException> sslContextProvider, + final HostnameVerifier verifier) { + this(config, sslContextProvider, verifier, null); + } + + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContext jersey client SSL context. + * @param verifier jersey client host name verifier. + * @param defaultSslContextProvider default SSL context provider. + */ + protected JerseyClient(final Configuration config, + final SSLContext sslContext, + final HostnameVerifier verifier, + final DefaultSslContextProvider defaultSslContextProvider, + ExecutorService executorService, + ScheduledExecutorService scheduledExecutorService) { + this(config, sslContext == null ? null : Values.unsafe(sslContext), verifier, + defaultSslContextProvider, executorService, scheduledExecutorService); + } + + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContextProvider jersey client SSL context provider. + * @param verifier jersey client host name verifier. + */ + protected JerseyClient(final Configuration config, + final UnsafeValue<SSLContext, IllegalStateException> sslContextProvider, + final HostnameVerifier verifier, + ExecutorService executorService, + ScheduledExecutorService scheduledExecutorService) { + this(config, sslContextProvider, verifier, null, executorService, scheduledExecutorService); + } + + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContextProvider jersey client SSL context provider. Non {@code null} provider is expected to + * return non-default value. + * @param verifier jersey client host name verifier. + * @param defaultSslContextProvider default SSL context provider. + */ + protected JerseyClient(final Configuration config, + final UnsafeValue<SSLContext, IllegalStateException> sslContextProvider, + final HostnameVerifier verifier, + final DefaultSslContextProvider defaultSslContextProvider) { + this(config, sslContextProvider, verifier, defaultSslContextProvider, null, null); + } + + protected JerseyClient(final Configuration config, + final UnsafeValue<SSLContext, IllegalStateException> sslContextProvider, + final HostnameVerifier verifier, + final DefaultSslContextProvider defaultSslContextProvider, + ExecutorService executorService, + ScheduledExecutorService scheduledExecutorService) { + this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config); + + if (sslContextProvider == null) { + this.isDefaultSslContext = true; + + if (defaultSslContextProvider != null) { + this.sslContext = createLazySslContext(defaultSslContextProvider); + } else { + final DefaultSslContextProvider lookedUpSslContextProvider; + + final Iterator<DefaultSslContextProvider> iterator = + ServiceFinder.find(DefaultSslContextProvider.class).iterator(); + + if (iterator.hasNext()) { + lookedUpSslContextProvider = iterator.next(); + } else { + lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER; + } + + this.sslContext = createLazySslContext(lookedUpSslContextProvider); + } + } else { + this.isDefaultSslContext = false; + this.sslContext = Values.lazy(sslContextProvider); + } + + this.hostnameVerifier = verifier; + this.executorService = executorService; + this.scheduledExecutorService = scheduledExecutorService; + } + + @Override + public void close() { + if (closedFlag.compareAndSet(false, true)) { + release(); + } + } + + private void release() { + Reference<ShutdownHook> listenerRef; + while ((listenerRef = shutdownHooks.pollFirst()) != null) { + JerseyClient.ShutdownHook listener = listenerRef.get(); + if (listener != null) { + try { + listener.onShutdown(); + } catch (Throwable t) { + LOG.log(Level.WARNING, LocalizationMessages.ERROR_SHUTDOWNHOOK_CLOSE(listenerRef.getClass().getName()), t); + } + } + } + } + + private UnsafeValue<SSLContext, IllegalStateException> createLazySslContext(final DefaultSslContextProvider provider) { + return Values.lazy(new UnsafeValue<SSLContext, IllegalStateException>() { + @Override + public SSLContext get() { + return provider.getDefaultSslContext(); + } + }); + } + + /** + * Register a new client shutdown hook. + * + * @param shutdownHook client shutdown hook. + */ + /* package */ void registerShutdownHook(final ShutdownHook shutdownHook) { + checkNotClosed(); + shutdownHooks.push(new WeakReference<JerseyClient.ShutdownHook>(shutdownHook, shReferenceQueue)); + cleanUpShutdownHooks(); + } + + /** + * Clean up shutdown hooks that have been garbage collected. + */ + private void cleanUpShutdownHooks() { + + Reference<? extends ShutdownHook> reference; + + while ((reference = shReferenceQueue.poll()) != null) { + + shutdownHooks.remove(reference); + + final ShutdownHook shutdownHook = reference.get(); + if (shutdownHook != null) { // close this one off if still accessible + shutdownHook.onShutdown(); + } + } + } + + private ScheduledExecutorService getDefaultScheduledExecutorService() { + return Executors.newScheduledThreadPool(8); + } + + /** + * Check client state. + * + * @return {@code true} if current {@code JerseyClient} instance is closed, otherwise {@code false}. + * + * @see #close() + */ + public boolean isClosed() { + return closedFlag.get(); + } + + /** + * Check that the client instance has not been closed. + * + * @throws IllegalStateException in case the client instance has been closed already. + */ + void checkNotClosed() { + checkState(!closedFlag.get(), LocalizationMessages.CLIENT_INSTANCE_CLOSED()); + } + + /** + * Get information about used {@link SSLContext}. + * + * @return {@code true} when used {@code SSLContext} is acquired from {@link SslConfigurator#getDefaultContext()}, + * {@code false} otherwise. + */ + public boolean isDefaultSslContext() { + return isDefaultSslContext; + } + + @Override + public JerseyWebTarget target(final String uri) { + checkNotClosed(); + checkNotNull(uri, LocalizationMessages.CLIENT_URI_TEMPLATE_NULL()); + return new JerseyWebTarget(uri, this); + } + + @Override + public JerseyWebTarget target(final URI uri) { + checkNotClosed(); + checkNotNull(uri, LocalizationMessages.CLIENT_URI_NULL()); + return new JerseyWebTarget(uri, this); + } + + @Override + public JerseyWebTarget target(final UriBuilder uriBuilder) { + checkNotClosed(); + checkNotNull(uriBuilder, LocalizationMessages.CLIENT_URI_BUILDER_NULL()); + return new JerseyWebTarget(uriBuilder, this); + } + + @Override + public JerseyWebTarget target(final Link link) { + checkNotClosed(); + checkNotNull(link, LocalizationMessages.CLIENT_TARGET_LINK_NULL()); + return new JerseyWebTarget(link, this); + } + + @Override + public JerseyInvocation.Builder invocation(final Link link) { + checkNotClosed(); + checkNotNull(link, LocalizationMessages.CLIENT_INVOCATION_LINK_NULL()); + final JerseyWebTarget t = new JerseyWebTarget(link, this); + final String acceptType = link.getType(); + return (acceptType != null) ? t.request(acceptType) : t.request(); + } + + @Override + public JerseyClient register(final Class<?> providerClass) { + checkNotClosed(); + config.register(providerClass); + return this; + } + + @Override + public JerseyClient register(final Object provider) { + checkNotClosed(); + config.register(provider); + return this; + } + + @Override + public JerseyClient register(final Class<?> providerClass, final int bindingPriority) { + checkNotClosed(); + config.register(providerClass, bindingPriority); + return this; + } + + @Override + public JerseyClient register(final Class<?> providerClass, final Class<?>... contracts) { + checkNotClosed(); + config.register(providerClass, contracts); + return this; + } + + @Override + public JerseyClient register(final Class<?> providerClass, final Map<Class<?>, Integer> contracts) { + checkNotClosed(); + config.register(providerClass, contracts); + return this; + } + + @Override + public JerseyClient register(final Object provider, final int bindingPriority) { + checkNotClosed(); + config.register(provider, bindingPriority); + return this; + } + + @Override + public JerseyClient register(final Object provider, final Class<?>... contracts) { + checkNotClosed(); + config.register(provider, contracts); + return this; + } + + @Override + public JerseyClient register(final Object provider, final Map<Class<?>, Integer> contracts) { + checkNotClosed(); + config.register(provider, contracts); + return this; + } + + @Override + public JerseyClient property(final String name, final Object value) { + checkNotClosed(); + config.property(name, value); + return this; + } + + @Override + public ClientConfig getConfiguration() { + checkNotClosed(); + return config.getConfiguration(); + } + + @Override + public SSLContext getSslContext() { + return sslContext.get(); + } + + @Override + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + public ExecutorService getExecutorService() { + return executorService; + } + + public ScheduledExecutorService getScheduledExecutorService() { + return scheduledExecutorService; + } + + @Override + public JerseyClient preInitialize() { + config.preInitialize(); + return this; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java new file mode 100644 index 0000000..b01e8a0 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
@@ -0,0 +1,243 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.security.KeyStore; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Configuration; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.util.collection.UnsafeValue; +import org.glassfish.jersey.internal.util.collection.Values; + +/** + * Jersey provider of {@link javax.ws.rs.client.ClientBuilder JAX-RS client builder}. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JerseyClientBuilder extends ClientBuilder { + + private final ClientConfig config; + private HostnameVerifier hostnameVerifier; + private SslConfigurator sslConfigurator; + private SSLContext sslContext; + private ExecutorService executorService; + private ScheduledExecutorService scheduledExecutorService; + + /** + * Create a new custom-configured {@link JerseyClient} instance. + * + * @return new configured Jersey client instance. + * @since 2.5 + */ + public static JerseyClient createClient() { + return new JerseyClientBuilder().build(); + } + + /** + * Create a new custom-configured {@link JerseyClient} instance. + * + * @param configuration data used to provide initial configuration for the new + * Jersey client instance. + * @return new configured Jersey client instance. + * @since 2.5 + */ + public static JerseyClient createClient(Configuration configuration) { + return new JerseyClientBuilder().withConfig(configuration).build(); + } + + /** + * Create new Jersey client builder instance. + */ + public JerseyClientBuilder() { + this.config = new ClientConfig(); + } + + @Override + public JerseyClientBuilder sslContext(SSLContext sslContext) { + if (sslContext == null) { + throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT()); + } + this.sslContext = sslContext; + sslConfigurator = null; + return this; + } + + @Override + public JerseyClientBuilder keyStore(KeyStore keyStore, char[] password) { + if (keyStore == null) { + throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE()); + } + if (password == null) { + throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD()); + } + if (sslConfigurator == null) { + sslConfigurator = SslConfigurator.newInstance(); + } + sslConfigurator.keyStore(keyStore); + sslConfigurator.keyPassword(password); + sslContext = null; + return this; + } + + @Override + public JerseyClientBuilder trustStore(KeyStore trustStore) { + if (trustStore == null) { + throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE()); + } + if (sslConfigurator == null) { + sslConfigurator = SslConfigurator.newInstance(); + } + sslConfigurator.trustStore(trustStore); + sslContext = null; + return this; + } + + @Override + public JerseyClientBuilder hostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + @Override + public ClientBuilder executorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + @Override + public ClientBuilder scheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = scheduledExecutorService; + return this; + } + + @Override + public ClientBuilder connectTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) { + throw new IllegalArgumentException("Negative timeout."); + } + + this.property(ClientProperties.CONNECT_TIMEOUT, Math.toIntExact(unit.toMillis(timeout))); + return this; + } + + @Override + public ClientBuilder readTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) { + throw new IllegalArgumentException("Negative timeout."); + } + + this.property(ClientProperties.READ_TIMEOUT, Math.toIntExact(unit.toMillis(timeout))); + return this; + } + + @Override + public JerseyClient build() { + if (sslContext != null) { + return new JerseyClient(config, sslContext, hostnameVerifier, null, executorService, + scheduledExecutorService); + } else if (sslConfigurator != null) { + final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy(); + return new JerseyClient( + config, + Values.lazy(new UnsafeValue<SSLContext, IllegalStateException>() { + @Override + public SSLContext get() { + return sslConfiguratorCopy.createSSLContext(); + } + }), + hostnameVerifier, executorService, scheduledExecutorService); + } else { + return new JerseyClient(config, (UnsafeValue<SSLContext, IllegalStateException>) null, hostnameVerifier, + executorService, scheduledExecutorService); + } + } + + @Override + public ClientConfig getConfiguration() { + return config; + } + + @Override + public JerseyClientBuilder property(String name, Object value) { + this.config.property(name, value); + return this; + } + + @Override + public JerseyClientBuilder register(Class<?> componentClass) { + this.config.register(componentClass); + return this; + } + + @Override + public JerseyClientBuilder register(Class<?> componentClass, int priority) { + this.config.register(componentClass, priority); + return this; + } + + @Override + public JerseyClientBuilder register(Class<?> componentClass, Class<?>... contracts) { + this.config.register(componentClass, contracts); + return this; + } + + @Override + public JerseyClientBuilder register(Class<?> componentClass, Map<Class<?>, Integer> contracts) { + this.config.register(componentClass, contracts); + return this; + } + + @Override + public JerseyClientBuilder register(Object component) { + this.config.register(component); + return this; + } + + @Override + public JerseyClientBuilder register(Object component, int priority) { + this.config.register(component, priority); + return this; + } + + @Override + public JerseyClientBuilder register(Object component, Class<?>... contracts) { + this.config.register(component, contracts); + return this; + } + + @Override + public JerseyClientBuilder register(Object component, Map<Class<?>, Integer> contracts) { + this.config.register(component, contracts); + return this; + } + + @Override + public JerseyClientBuilder withConfig(Configuration config) { + this.config.loadFrom(config); + return this; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyCompletionStageRxInvoker.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyCompletionStageRxInvoker.java new file mode 100644 index 0000000..28531ed --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyCompletionStageRxInvoker.java
@@ -0,0 +1,57 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; + +import javax.ws.rs.client.CompletionStageRxInvoker; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.GenericType; + +/** + * Implementation of Reactive Invoker for {@code CompletionStage}. + * + * @author Michal Gajdos + * @since 2.26 + */ +public class JerseyCompletionStageRxInvoker extends AbstractRxInvoker<CompletionStage> implements CompletionStageRxInvoker { + + JerseyCompletionStageRxInvoker(Invocation.Builder builder, ExecutorService executor) { + super(builder, executor); + } + + @Override + public <T> CompletionStage<T> method(final String name, final Entity<?> entity, final Class<T> responseType) { + final ExecutorService executorService = getExecutorService(); + + return executorService == null + ? CompletableFuture.supplyAsync(() -> getSyncInvoker().method(name, entity, responseType)) + : CompletableFuture.supplyAsync(() -> getSyncInvoker().method(name, entity, responseType), executorService); + } + + @Override + public <T> CompletionStage<T> method(final String name, final Entity<?> entity, final GenericType<T> responseType) { + final ExecutorService executorService = getExecutorService(); + + return executorService == null + ? CompletableFuture.supplyAsync(() -> getSyncInvoker().method(name, entity, responseType)) + : CompletableFuture.supplyAsync(() -> getSyncInvoker().method(name, entity, responseType), executorService); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java new file mode 100644 index 0000000..b2dfddd --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java
@@ -0,0 +1,1117 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.reflect.Type; +import java.net.URI; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.logging.Logger; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotAcceptableException; +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.NotSupportedException; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.RedirectionException; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.ServiceUnavailableException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.CompletionStageRxInvoker; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.client.ResponseProcessingException; +import javax.ws.rs.client.RxInvoker; +import javax.ws.rs.client.RxInvokerProvider; +import javax.ws.rs.client.SyncInvoker; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.util.Producer; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.process.internal.RequestScope; +import org.glassfish.jersey.spi.ExecutorServiceProvider; + +/** + * Jersey implementation of {@link javax.ws.rs.client.Invocation JAX-RS client-side + * request invocation} contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JerseyInvocation implements javax.ws.rs.client.Invocation { + + private static final Logger LOGGER = Logger.getLogger(JerseyInvocation.class.getName()); + + private final ClientRequest requestContext; + // Copy request context when invoke or submit methods are invoked. + private final boolean copyRequestContext; + + private JerseyInvocation(final Builder builder) { + this(builder, false); + } + + private JerseyInvocation(final Builder builder, final boolean copyRequestContext) { + validateHttpMethodAndEntity(builder.requestContext); + + this.requestContext = new ClientRequest(builder.requestContext); + this.copyRequestContext = copyRequestContext; + } + + private enum EntityPresence { + MUST_BE_NULL, + MUST_BE_PRESENT, + OPTIONAL + } + + private static final Map<String, EntityPresence> METHODS = initializeMap(); + + private static Map<String, EntityPresence> initializeMap() { + final Map<String, EntityPresence> map = new HashMap<>(); + + map.put("DELETE", EntityPresence.MUST_BE_NULL); + map.put("GET", EntityPresence.MUST_BE_NULL); + map.put("HEAD", EntityPresence.MUST_BE_NULL); + map.put("OPTIONS", EntityPresence.MUST_BE_NULL); + map.put("PATCH", EntityPresence.MUST_BE_PRESENT); + map.put("POST", EntityPresence.OPTIONAL); // we allow to post null instead of entity + map.put("PUT", EntityPresence.MUST_BE_PRESENT); + map.put("TRACE", EntityPresence.MUST_BE_NULL); + return map; + } + + private void validateHttpMethodAndEntity(final ClientRequest request) { + boolean suppressExceptions; + suppressExceptions = PropertiesHelper.isProperty( + request.getConfiguration().getProperty(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION)); + + final Object shcvProperty = request.getProperty(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION); + if (shcvProperty != null) { // override global configuration with request-specific + suppressExceptions = PropertiesHelper.isProperty(shcvProperty); + } + + final String method = request.getMethod(); + + final EntityPresence entityPresence = METHODS.get(method.toUpperCase()); + if (entityPresence == EntityPresence.MUST_BE_NULL && request.hasEntity()) { + if (suppressExceptions) { + LOGGER.warning(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NOT_NULL(method)); + } else { + throw new IllegalStateException(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NOT_NULL(method)); + } + } else if (entityPresence == EntityPresence.MUST_BE_PRESENT && !request.hasEntity()) { + if (suppressExceptions) { + LOGGER.warning(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NULL(method)); + } else { + throw new IllegalStateException(LocalizationMessages.ERROR_HTTP_METHOD_ENTITY_NULL(method)); + } + } + } + + /** + * Jersey-specific {@link javax.ws.rs.client.Invocation.Builder client invocation builder}. + */ + public static class Builder implements javax.ws.rs.client.Invocation.Builder { + + private final ClientRequest requestContext; + + /** + * Create new Jersey-specific client invocation builder. + * + * @param uri invoked request URI. + * @param configuration Jersey client configuration. + */ + protected Builder(final URI uri, final ClientConfig configuration) { + this.requestContext = new ClientRequest(uri, configuration, new MapPropertiesDelegate()); + } + + /** + * Returns a reference to the mutable request context to be invoked. + * + * @return mutable request context to be invoked. + */ + ClientRequest request() { + return requestContext; + } + + private void storeEntity(final Entity<?> entity) { + if (entity != null) { + requestContext.variant(entity.getVariant()); + requestContext.setEntity(entity.getEntity()); + requestContext.setEntityAnnotations(entity.getAnnotations()); + } + } + + @Override + public JerseyInvocation build(final String method) { + requestContext.setMethod(method); + return new JerseyInvocation(this, true); + } + + @Override + public JerseyInvocation build(final String method, final Entity<?> entity) { + requestContext.setMethod(method); + storeEntity(entity); + return new JerseyInvocation(this, true); + } + + @Override + public JerseyInvocation buildGet() { + requestContext.setMethod("GET"); + return new JerseyInvocation(this, true); + } + + @Override + public JerseyInvocation buildDelete() { + requestContext.setMethod("DELETE"); + return new JerseyInvocation(this, true); + } + + @Override + public JerseyInvocation buildPost(final Entity<?> entity) { + requestContext.setMethod("POST"); + storeEntity(entity); + return new JerseyInvocation(this, true); + } + + @Override + public JerseyInvocation buildPut(final Entity<?> entity) { + requestContext.setMethod("PUT"); + storeEntity(entity); + return new JerseyInvocation(this, true); + } + + @Override + public javax.ws.rs.client.AsyncInvoker async() { + return new AsyncInvoker(this); + } + + @Override + public Builder accept(final String... mediaTypes) { + requestContext.accept(mediaTypes); + return this; + } + + @Override + public Builder accept(final MediaType... mediaTypes) { + requestContext.accept(mediaTypes); + return this; + } + + @Override + public Invocation.Builder acceptEncoding(final String... encodings) { + requestContext.getHeaders().addAll(HttpHeaders.ACCEPT_ENCODING, (Object[]) encodings); + return this; + } + + @Override + public Builder acceptLanguage(final Locale... locales) { + requestContext.acceptLanguage(locales); + return this; + } + + @Override + public Builder acceptLanguage(final String... locales) { + requestContext.acceptLanguage(locales); + return this; + } + + @Override + public Builder cookie(final Cookie cookie) { + requestContext.cookie(cookie); + return this; + } + + @Override + public Builder cookie(final String name, final String value) { + requestContext.cookie(new Cookie(name, value)); + return this; + } + + @Override + public Builder cacheControl(final CacheControl cacheControl) { + requestContext.cacheControl(cacheControl); + return this; + } + + @Override + public Builder header(final String name, final Object value) { + final MultivaluedMap<String, Object> headers = requestContext.getHeaders(); + + if (value == null) { + headers.remove(name); + } else { + headers.add(name, value); + } + + if (HttpHeaders.USER_AGENT.equalsIgnoreCase(name)) { + requestContext.ignoreUserAgent(value == null); + } + + return this; + } + + @Override + public Builder headers(final MultivaluedMap<String, Object> headers) { + requestContext.replaceHeaders(headers); + return this; + } + + @Override + public Response get() throws ProcessingException { + return method("GET"); + } + + @Override + public <T> T get(final Class<T> responseType) throws ProcessingException, WebApplicationException { + return method("GET", responseType); + } + + @Override + public <T> T get(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { + return method("GET", responseType); + } + + @Override + public Response put(final Entity<?> entity) throws ProcessingException { + return method("PUT", entity); + } + + @Override + public <T> T put(final Entity<?> entity, final Class<T> responseType) + throws ProcessingException, WebApplicationException { + return method("PUT", entity, responseType); + } + + @Override + public <T> T put(final Entity<?> entity, final GenericType<T> responseType) + throws ProcessingException, WebApplicationException { + return method("PUT", entity, responseType); + } + + @Override + public Response post(final Entity<?> entity) throws ProcessingException { + return method("POST", entity); + } + + @Override + public <T> T post(final Entity<?> entity, final Class<T> responseType) + throws ProcessingException, WebApplicationException { + return method("POST", entity, responseType); + } + + @Override + public <T> T post(final Entity<?> entity, final GenericType<T> responseType) + throws ProcessingException, WebApplicationException { + return method("POST", entity, responseType); + } + + @Override + public Response delete() throws ProcessingException { + return method("DELETE"); + } + + @Override + public <T> T delete(final Class<T> responseType) throws ProcessingException, WebApplicationException { + return method("DELETE", responseType); + } + + @Override + public <T> T delete(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { + return method("DELETE", responseType); + } + + @Override + public Response head() throws ProcessingException { + return method("HEAD"); + } + + @Override + public Response options() throws ProcessingException { + return method("OPTIONS"); + } + + @Override + public <T> T options(final Class<T> responseType) throws ProcessingException, WebApplicationException { + return method("OPTIONS", responseType); + } + + @Override + public <T> T options(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { + return method("OPTIONS", responseType); + } + + @Override + public Response trace() throws ProcessingException { + return method("TRACE"); + } + + @Override + public <T> T trace(final Class<T> responseType) throws ProcessingException, WebApplicationException { + return method("TRACE", responseType); + } + + @Override + public <T> T trace(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { + return method("TRACE", responseType); + } + + @Override + public Response method(final String name) throws ProcessingException { + requestContext.setMethod(name); + return new JerseyInvocation(this).invoke(); + } + + @Override + public <T> T method(final String name, final Class<T> responseType) throws ProcessingException, WebApplicationException { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + requestContext.setMethod(name); + return new JerseyInvocation(this).invoke(responseType); + } + + @Override + public <T> T method(final String name, final GenericType<T> responseType) + throws ProcessingException, WebApplicationException { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + requestContext.setMethod(name); + return new JerseyInvocation(this).invoke(responseType); + } + + @Override + public Response method(final String name, final Entity<?> entity) throws ProcessingException { + requestContext.setMethod(name); + storeEntity(entity); + return new JerseyInvocation(this).invoke(); + } + + @Override + public <T> T method(final String name, final Entity<?> entity, final Class<T> responseType) + throws ProcessingException, WebApplicationException { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + requestContext.setMethod(name); + storeEntity(entity); + return new JerseyInvocation(this).invoke(responseType); + } + + @Override + public <T> T method(final String name, final Entity<?> entity, final GenericType<T> responseType) + throws ProcessingException, WebApplicationException { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + requestContext.setMethod(name); + storeEntity(entity); + return new JerseyInvocation(this).invoke(responseType); + } + + @Override + public Builder property(final String name, final Object value) { + requestContext.setProperty(name, value); + return this; + } + + @Override + public CompletionStageRxInvoker rx() { + ExecutorServiceProvider instance = this.requestContext.getInjectionManager() + .getInstance(ExecutorServiceProvider.class); + + return new JerseyCompletionStageRxInvoker(this, instance.getExecutorService()); + } + + @Override + public <T extends RxInvoker> T rx(Class<T> clazz) { + ExecutorServiceProvider instance = this.requestContext.getInjectionManager() + .getInstance(ExecutorServiceProvider.class); + + return createRxInvoker(clazz, instance.getExecutorService()); + } + + private <T extends RxInvoker> T rx(Class<T> clazz, ExecutorService executorService) { + if (executorService == null) { + throw new IllegalArgumentException(LocalizationMessages.NULL_INPUT_PARAMETER("executorService")); + } + + return createRxInvoker(clazz, executorService); + } + + /** + * Create {@link RxInvoker} from provided {@code RxInvoker} subclass. + * <p> + * The method does a lookup for {@link RxInvokerProvider}, which provides given {@code RxInvoker} subclass + * and if found, calls {@link RxInvokerProvider#getRxInvoker(SyncInvoker, ExecutorService)} + * + * @param clazz {@code RxInvoker} subclass to be created. + * @param executorService to be passed to the factory method invocation. + * @param <T> {@code RxInvoker} subclass to be returned. + * @return thread safe instance of {@code RxInvoker} subclass. + * @throws IllegalStateException when provider for given class is not registered. + */ + private <T extends RxInvoker> T createRxInvoker(Class<? extends RxInvoker> clazz, + ExecutorService executorService) { + if (clazz == null) { + throw new IllegalArgumentException(LocalizationMessages.NULL_INPUT_PARAMETER("clazz")); + } + + Iterable<RxInvokerProvider> allProviders = Providers.getAllProviders( + this.requestContext.getInjectionManager(), + RxInvokerProvider.class); + + for (RxInvokerProvider invokerProvider : allProviders) { + if (invokerProvider.isProviderFor(clazz)) { + + RxInvoker rxInvoker = invokerProvider.getRxInvoker(this, executorService); + + if (rxInvoker == null) { + throw new IllegalStateException(LocalizationMessages.CLIENT_RX_PROVIDER_NULL()); + } + + return (T) rxInvoker; + } + } + + throw new IllegalStateException( + LocalizationMessages.CLIENT_RX_PROVIDER_NOT_REGISTERED(clazz.getSimpleName())); + } + } + + private static class AsyncInvoker implements javax.ws.rs.client.AsyncInvoker { + + private final JerseyInvocation.Builder builder; + + private AsyncInvoker(final JerseyInvocation.Builder request) { + this.builder = request; + this.builder.requestContext.setAsynchronous(true); + } + + @Override + public Future<Response> get() { + return method("GET"); + } + + @Override + public <T> Future<T> get(final Class<T> responseType) { + return method("GET", responseType); + } + + @Override + public <T> Future<T> get(final GenericType<T> responseType) { + return method("GET", responseType); + } + + @Override + public <T> Future<T> get(final InvocationCallback<T> callback) { + return method("GET", callback); + } + + @Override + public Future<Response> put(final Entity<?> entity) { + return method("PUT", entity); + } + + @Override + public <T> Future<T> put(final Entity<?> entity, final Class<T> responseType) { + return method("PUT", entity, responseType); + } + + @Override + public <T> Future<T> put(final Entity<?> entity, final GenericType<T> responseType) { + return method("PUT", entity, responseType); + } + + @Override + public <T> Future<T> put(final Entity<?> entity, final InvocationCallback<T> callback) { + return method("PUT", entity, callback); + } + + @Override + public Future<Response> post(final Entity<?> entity) { + return method("POST", entity); + } + + @Override + public <T> Future<T> post(final Entity<?> entity, final Class<T> responseType) { + return method("POST", entity, responseType); + } + + @Override + public <T> Future<T> post(final Entity<?> entity, final GenericType<T> responseType) { + return method("POST", entity, responseType); + } + + @Override + public <T> Future<T> post(final Entity<?> entity, final InvocationCallback<T> callback) { + return method("POST", entity, callback); + } + + @Override + public Future<Response> delete() { + return method("DELETE"); + } + + @Override + public <T> Future<T> delete(final Class<T> responseType) { + return method("DELETE", responseType); + } + + @Override + public <T> Future<T> delete(final GenericType<T> responseType) { + return method("DELETE", responseType); + } + + @Override + public <T> Future<T> delete(final InvocationCallback<T> callback) { + return method("DELETE", callback); + } + + @Override + public Future<Response> head() { + return method("HEAD"); + } + + @Override + public Future<Response> head(final InvocationCallback<Response> callback) { + return method("HEAD", callback); + } + + @Override + public Future<Response> options() { + return method("OPTIONS"); + } + + @Override + public <T> Future<T> options(final Class<T> responseType) { + return method("OPTIONS", responseType); + } + + @Override + public <T> Future<T> options(final GenericType<T> responseType) { + return method("OPTIONS", responseType); + } + + @Override + public <T> Future<T> options(final InvocationCallback<T> callback) { + return method("OPTIONS", callback); + } + + @Override + public Future<Response> trace() { + return method("TRACE"); + } + + @Override + public <T> Future<T> trace(final Class<T> responseType) { + return method("TRACE", responseType); + } + + @Override + public <T> Future<T> trace(final GenericType<T> responseType) { + return method("TRACE", responseType); + } + + @Override + public <T> Future<T> trace(final InvocationCallback<T> callback) { + return method("TRACE", callback); + } + + @Override + public Future<Response> method(final String name) { + builder.requestContext.setMethod(name); + return new JerseyInvocation(builder).submit(); + } + + @Override + public <T> Future<T> method(final String name, final Class<T> responseType) { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + builder.requestContext.setMethod(name); + return new JerseyInvocation(builder).submit(responseType); + } + + @Override + public <T> Future<T> method(final String name, final GenericType<T> responseType) { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + builder.requestContext.setMethod(name); + return new JerseyInvocation(builder).submit(responseType); + } + + @Override + public <T> Future<T> method(final String name, final InvocationCallback<T> callback) { + builder.requestContext.setMethod(name); + return new JerseyInvocation(builder).submit(callback); + } + + @Override + public Future<Response> method(final String name, final Entity<?> entity) { + builder.requestContext.setMethod(name); + builder.storeEntity(entity); + return new JerseyInvocation(builder).submit(); + } + + @Override + public <T> Future<T> method(final String name, final Entity<?> entity, final Class<T> responseType) { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + builder.requestContext.setMethod(name); + builder.storeEntity(entity); + return new JerseyInvocation(builder).submit(responseType); + } + + @Override + public <T> Future<T> method(final String name, final Entity<?> entity, final GenericType<T> responseType) { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + builder.requestContext.setMethod(name); + builder.storeEntity(entity); + return new JerseyInvocation(builder).submit(responseType); + } + + @Override + public <T> Future<T> method(final String name, final Entity<?> entity, final InvocationCallback<T> callback) { + builder.requestContext.setMethod(name); + builder.storeEntity(entity); + return new JerseyInvocation(builder).submit(callback); + } + } + + private ClientRequest requestForCall(final ClientRequest requestContext) { + return copyRequestContext ? new ClientRequest(requestContext) : requestContext; + } + + @Override + public Response invoke() throws ProcessingException, WebApplicationException { + final ClientRuntime runtime = request().getClientRuntime(); + final RequestScope requestScope = runtime.getRequestScope(); + return requestScope.runInScope( + (Producer<Response>) () -> new InboundJaxrsResponse(runtime.invoke(requestForCall(requestContext)), + requestScope)); + } + + @Override + public <T> T invoke(final Class<T> responseType) throws ProcessingException, WebApplicationException { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + final ClientRuntime runtime = request().getClientRuntime(); + final RequestScope requestScope = runtime.getRequestScope(); + //noinspection Duplicates + return requestScope.runInScope(() -> { + try { + return translate(runtime.invoke(requestForCall(requestContext)), requestScope, responseType); + } catch (final ProcessingException ex) { + if (ex.getCause() instanceof WebApplicationException) { + throw (WebApplicationException) ex.getCause(); + } + throw ex; + } + }); + } + + @Override + public <T> T invoke(final GenericType<T> responseType) throws ProcessingException, WebApplicationException { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + final ClientRuntime runtime = request().getClientRuntime(); + final RequestScope requestScope = runtime.getRequestScope(); + //noinspection Duplicates + return requestScope.runInScope(() -> { + try { + return translate(runtime.invoke(requestForCall(requestContext)), requestScope, responseType); + } catch (final ProcessingException ex) { + if (ex.getCause() instanceof WebApplicationException) { + throw (WebApplicationException) ex.getCause(); + } + throw ex; + } + }); + } + + @Override + public Future<Response> submit() { + final CompletableFuture<Response> responseFuture = new CompletableFuture<>(); + final ClientRuntime runtime = request().getClientRuntime(); + runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), new ResponseCallback() { + + @Override + public void completed(final ClientResponse response, final RequestScope scope) { + if (!responseFuture.isCancelled()) { + responseFuture.complete(new InboundJaxrsResponse(response, scope)); + } else { + response.close(); + } + } + + @Override + public void failed(final ProcessingException error) { + if (!responseFuture.isCancelled()) { + responseFuture.completeExceptionally(error); + } + } + })); + + return responseFuture; + } + + @Override + public <T> Future<T> submit(final Class<T> responseType) { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + final CompletableFuture<T> responseFuture = new CompletableFuture<>(); + //noinspection Duplicates + final ClientRuntime runtime = request().getClientRuntime(); + runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), new ResponseCallback() { + + @Override + public void completed(final ClientResponse response, final RequestScope scope) { + if (responseFuture.isCancelled()) { + response.close(); + return; + } + try { + responseFuture.complete(translate(response, scope, responseType)); + } catch (final ProcessingException ex) { + failed(ex); + } + } + + @Override + public void failed(final ProcessingException error) { + if (responseFuture.isCancelled()) { + return; + } + if (error.getCause() instanceof WebApplicationException) { + responseFuture.completeExceptionally(error.getCause()); + } else { + responseFuture.completeExceptionally(error); + } + } + })); + + return responseFuture; + } + + private <T> T translate(final ClientResponse response, final RequestScope scope, final Class<T> responseType) + throws ProcessingException { + if (responseType == Response.class) { + return responseType.cast(new InboundJaxrsResponse(response, scope)); + } + + if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { + try { + return response.readEntity(responseType); + } catch (final ProcessingException ex) { + if (ex.getClass() == ProcessingException.class) { + throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex.getCause()); + } + throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex); + } catch (final WebApplicationException ex) { + throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex); + } catch (final Exception ex) { + throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), + LocalizationMessages.UNEXPECTED_ERROR_RESPONSE_PROCESSING(), ex); + } + } else { + throw convertToException(new InboundJaxrsResponse(response, scope)); + } + } + + @Override + public <T> Future<T> submit(final GenericType<T> responseType) { + if (responseType == null) { + throw new IllegalArgumentException(LocalizationMessages.RESPONSE_TYPE_IS_NULL()); + } + final CompletableFuture<T> responseFuture = new CompletableFuture<>(); + //noinspection Duplicates + final ClientRuntime runtime = request().getClientRuntime(); + runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), new ResponseCallback() { + + @Override + public void completed(final ClientResponse response, final RequestScope scope) { + if (responseFuture.isCancelled()) { + response.close(); + return; + } + + try { + responseFuture.complete(translate(response, scope, responseType)); + } catch (final ProcessingException ex) { + failed(ex); + } + } + + @Override + public void failed(final ProcessingException error) { + if (responseFuture.isCancelled()) { + return; + } + if (error.getCause() instanceof WebApplicationException) { + responseFuture.completeExceptionally(error.getCause()); + } else { + responseFuture.completeExceptionally(error); + } + } + })); + + return responseFuture; + } + + private <T> T translate(final ClientResponse response, final RequestScope scope, final GenericType<T> responseType) + throws ProcessingException { + if (responseType.getRawType() == Response.class) { + //noinspection unchecked + return (T) new InboundJaxrsResponse(response, scope); + } + + if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { + try { + return response.readEntity(responseType); + } catch (final ProcessingException ex) { + throw new ResponseProcessingException( + new InboundJaxrsResponse(response, scope), + ex.getCause() != null ? ex.getCause() : ex); + } catch (final WebApplicationException ex) { + throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), ex); + } catch (final Exception ex) { + throw new ResponseProcessingException(new InboundJaxrsResponse(response, scope), + LocalizationMessages.UNEXPECTED_ERROR_RESPONSE_PROCESSING(), ex); + } + } else { + throw convertToException(new InboundJaxrsResponse(response, scope)); + } + } + + @Override + public <T> Future<T> submit(final InvocationCallback<T> callback) { + return submit(null, callback); + } + + /** + * Submit the request for an asynchronous invocation and register an + * {@link InvocationCallback} to process the future result of the invocation. + * <p> + * Response type in this case is taken from {@code responseType} param (if not {@code null}) rather + * than from {@code callback}. This allows to pass callbacks like {@code new InvocationCallback<>() {...}}. + * </p> + * + * @param <T> response type + * @param responseType response type that is used instead of obtaining types from {@code callback}. + * @param callback invocation callback for asynchronous processing of the + * request invocation result. + * @return future response object of the specified type as a result of the + * request invocation. + */ + public <T> Future<T> submit(final GenericType<T> responseType, final InvocationCallback<T> callback) { + final CompletableFuture<T> responseFuture = new CompletableFuture<>(); + + try { + final ReflectionHelper.DeclaringClassInterfacePair pair = + ReflectionHelper.getClass(callback.getClass(), InvocationCallback.class); + + final Type callbackParamType; + final Class<T> callbackParamClass; + + if (responseType == null) { + // If we don't have response use callback to obtain param types. + final Type[] typeArguments = ReflectionHelper.getParameterizedTypeArguments(pair); + if (typeArguments == null || typeArguments.length == 0) { + callbackParamType = Object.class; + } else { + callbackParamType = typeArguments[0]; + } + callbackParamClass = ReflectionHelper.erasure(callbackParamType); + } else { + callbackParamType = responseType.getType(); + callbackParamClass = ReflectionHelper.erasure(responseType.getRawType()); + } + + final ResponseCallback responseCallback = new ResponseCallback() { + + @Override + public void completed(final ClientResponse response, final RequestScope scope) { + if (responseFuture.isCancelled()) { + response.close(); + failed(new ProcessingException( + new CancellationException(LocalizationMessages.ERROR_REQUEST_CANCELLED()))); + return; + } + + final T result; + if (callbackParamClass == Response.class) { + result = callbackParamClass.cast(new InboundJaxrsResponse(response, scope)); + responseFuture.complete(result); + callback.completed(result); + } else if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { + result = response.readEntity(new GenericType<T>(callbackParamType)); + responseFuture.complete(result); + callback.completed(result); + } else { + failed(convertToException(new InboundJaxrsResponse(response, scope))); + } + } + + @Override + public void failed(final ProcessingException error) { + try { + if (error.getCause() instanceof WebApplicationException) { + responseFuture.completeExceptionally(error.getCause()); + } else if (!responseFuture.isCancelled()) { + responseFuture.completeExceptionally(error); + } + } finally { + callback.failed(error.getCause() instanceof CancellationException ? error.getCause() : error); + } + } + }; + final ClientRuntime runtime = request().getClientRuntime(); + runtime.submit(runtime.createRunnableForAsyncProcessing(requestForCall(requestContext), responseCallback)); + } catch (final Throwable error) { + final ProcessingException ce; + //noinspection ChainOfInstanceofChecks + if (error instanceof ProcessingException) { + ce = (ProcessingException) error; + responseFuture.completeExceptionally(ce); + } else if (error instanceof WebApplicationException) { + ce = new ProcessingException(error); + responseFuture.completeExceptionally(error); + } else { + ce = new ProcessingException(error); + responseFuture.completeExceptionally(ce); + } + callback.failed(ce); + } + + return responseFuture; + } + + @Override + public JerseyInvocation property(final String name, final Object value) { + requestContext.setProperty(name, value); + return this; + } + + private ProcessingException convertToException(final Response response) { + try { + // Buffer and close entity input stream (if any) to prevent + // leaking connections (see JERSEY-2157). + response.bufferEntity(); + + final WebApplicationException webAppException; + final int statusCode = response.getStatus(); + final Response.Status status = Response.Status.fromStatusCode(statusCode); + + if (status == null) { + final Response.Status.Family statusFamily = response.getStatusInfo().getFamily(); + webAppException = createExceptionForFamily(response, statusFamily); + } else { + switch (status) { + case BAD_REQUEST: + webAppException = new BadRequestException(response); + break; + case UNAUTHORIZED: + webAppException = new NotAuthorizedException(response); + break; + case FORBIDDEN: + webAppException = new ForbiddenException(response); + break; + case NOT_FOUND: + webAppException = new NotFoundException(response); + break; + case METHOD_NOT_ALLOWED: + webAppException = new NotAllowedException(response); + break; + case NOT_ACCEPTABLE: + webAppException = new NotAcceptableException(response); + break; + case UNSUPPORTED_MEDIA_TYPE: + webAppException = new NotSupportedException(response); + break; + case INTERNAL_SERVER_ERROR: + webAppException = new InternalServerErrorException(response); + break; + case SERVICE_UNAVAILABLE: + webAppException = new ServiceUnavailableException(response); + break; + default: + final Response.Status.Family statusFamily = response.getStatusInfo().getFamily(); + webAppException = createExceptionForFamily(response, statusFamily); + } + } + + return new ResponseProcessingException(response, webAppException); + } catch (final Throwable t) { + return new ResponseProcessingException(response, LocalizationMessages.RESPONSE_TO_EXCEPTION_CONVERSION_FAILED(), t); + } + } + + private WebApplicationException createExceptionForFamily(final Response response, final Response.Status.Family statusFamily) { + final WebApplicationException webAppException; + switch (statusFamily) { + case REDIRECTION: + webAppException = new RedirectionException(response); + break; + case CLIENT_ERROR: + webAppException = new ClientErrorException(response); + break; + case SERVER_ERROR: + webAppException = new ServerErrorException(response); + break; + default: + webAppException = new WebApplicationException(response); + } + return webAppException; + } + + /** + * Returns a reference to the mutable request context to be invoked. + * + * @return mutable request context to be invoked. + */ + ClientRequest request() { + return requestContext; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java new file mode 100644 index 0000000..c7d54be --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
@@ -0,0 +1,361 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.Link; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.internal.guava.Preconditions; + +/** + * Jersey implementation of {@link javax.ws.rs.client.WebTarget JAX-RS client target} + * contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JerseyWebTarget implements javax.ws.rs.client.WebTarget, Initializable<JerseyWebTarget> { + + private final ClientConfig config; + private final UriBuilder targetUri; + + /** + * Create new web target instance. + * + * @param uri target URI. + * @param parent parent client. + */ + /*package*/ JerseyWebTarget(String uri, JerseyClient parent) { + this(UriBuilder.fromUri(uri), parent.getConfiguration()); + } + + /** + * Create new web target instance. + * + * @param uri target URI. + * @param parent parent client. + */ + /*package*/ JerseyWebTarget(URI uri, JerseyClient parent) { + this(UriBuilder.fromUri(uri), parent.getConfiguration()); + } + + /** + * Create new web target instance. + * + * @param uriBuilder builder for the target URI. + * @param parent parent client. + */ + /*package*/ JerseyWebTarget(UriBuilder uriBuilder, JerseyClient parent) { + this(uriBuilder.clone(), parent.getConfiguration()); + } + + /** + * Create new web target instance. + * + * @param link link to the target URI. + * @param parent parent client. + */ + /*package*/ JerseyWebTarget(Link link, JerseyClient parent) { + // TODO handle relative links + this(UriBuilder.fromUri(link.getUri()), parent.getConfiguration()); + } + + /** + * Create new web target instance. + * + * @param uriBuilder builder for the target URI. + * @param that original target to copy the internal data from. + */ + protected JerseyWebTarget(UriBuilder uriBuilder, JerseyWebTarget that) { + this(uriBuilder, that.config); + } + + /** + * Create new web target instance. + * + * @param uriBuilder builder for the target URI. + * @param clientConfig target configuration. + */ + protected JerseyWebTarget(UriBuilder uriBuilder, ClientConfig clientConfig) { + clientConfig.checkClient(); + + this.targetUri = uriBuilder; + this.config = clientConfig.snapshot(); + } + + @Override + public URI getUri() { + checkNotClosed(); + try { + return targetUri.build(); + } catch (IllegalArgumentException ex) { + throw new IllegalStateException(ex.getMessage(), ex); + } + } + + private void checkNotClosed() { + config.getClient().checkNotClosed(); + } + + @Override + public UriBuilder getUriBuilder() { + checkNotClosed(); + return targetUri.clone(); + } + + @Override + public JerseyWebTarget path(String path) throws NullPointerException { + checkNotClosed(); + Preconditions.checkNotNull(path, "path is 'null'."); + + return new JerseyWebTarget(getUriBuilder().path(path), this); + } + + @Override + public JerseyWebTarget matrixParam(String name, Object... values) throws NullPointerException { + checkNotClosed(); + Preconditions.checkNotNull(name, "Matrix parameter name must not be 'null'."); + + if (values == null || values.length == 0 || (values.length == 1 && values[0] == null)) { + return new JerseyWebTarget(getUriBuilder().replaceMatrixParam(name, (Object[]) null), this); + } + + checkForNullValues(name, values); + return new JerseyWebTarget(getUriBuilder().matrixParam(name, values), this); + } + + @Override + public JerseyWebTarget queryParam(String name, Object... values) throws NullPointerException { + checkNotClosed(); + return new JerseyWebTarget(JerseyWebTarget.setQueryParam(getUriBuilder(), name, values), this); + } + + private static UriBuilder setQueryParam(UriBuilder uriBuilder, String name, Object[] values) { + if (values == null || values.length == 0 || (values.length == 1 && values[0] == null)) { + return uriBuilder.replaceQueryParam(name, (Object[]) null); + } + + checkForNullValues(name, values); + return uriBuilder.queryParam(name, values); + } + + private static void checkForNullValues(String name, Object[] values) { + Preconditions.checkNotNull(name, "name is 'null'."); + + List<Integer> indexes = new LinkedList<Integer>(); + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + indexes.add(i); + } + } + final int failedIndexCount = indexes.size(); + if (failedIndexCount > 0) { + final String valueTxt; + final String indexTxt; + if (failedIndexCount == 1) { + valueTxt = "value"; + indexTxt = "index"; + } else { + valueTxt = "values"; + indexTxt = "indexes"; + } + + throw new NullPointerException( + String.format("'null' %s detected for parameter '%s' on %s : %s", + valueTxt, name, indexTxt, indexes.toString())); + } + } + + @Override + public JerseyInvocation.Builder request() { + checkNotClosed(); + return new JerseyInvocation.Builder(getUri(), config.snapshot()); + } + + @Override + public JerseyInvocation.Builder request(String... acceptedResponseTypes) { + checkNotClosed(); + JerseyInvocation.Builder b = new JerseyInvocation.Builder(getUri(), config.snapshot()); + b.request().accept(acceptedResponseTypes); + return b; + } + + @Override + public JerseyInvocation.Builder request(MediaType... acceptedResponseTypes) { + checkNotClosed(); + JerseyInvocation.Builder b = new JerseyInvocation.Builder(getUri(), config.snapshot()); + b.request().accept(acceptedResponseTypes); + return b; + } + + @Override + public JerseyWebTarget resolveTemplate(String name, Object value) throws NullPointerException { + return resolveTemplate(name, value, true); + } + + @Override + public JerseyWebTarget resolveTemplate(String name, Object value, boolean encodeSlashInPath) throws NullPointerException { + checkNotClosed(); + Preconditions.checkNotNull(name, "name is 'null'."); + Preconditions.checkNotNull(value, "value is 'null'."); + + return new JerseyWebTarget(getUriBuilder().resolveTemplate(name, value, encodeSlashInPath), this); + } + + @Override + public JerseyWebTarget resolveTemplateFromEncoded(String name, Object value) + throws NullPointerException { + checkNotClosed(); + Preconditions.checkNotNull(name, "name is 'null'."); + Preconditions.checkNotNull(value, "value is 'null'."); + + return new JerseyWebTarget(getUriBuilder().resolveTemplateFromEncoded(name, value), this); + } + + @Override + public JerseyWebTarget resolveTemplates(Map<String, Object> templateValues) throws NullPointerException { + return resolveTemplates(templateValues, true); + } + + @Override + public JerseyWebTarget resolveTemplates(Map<String, Object> templateValues, boolean encodeSlashInPath) + throws NullPointerException { + checkNotClosed(); + checkTemplateValues(templateValues); + + if (templateValues.isEmpty()) { + return this; + } else { + return new JerseyWebTarget(getUriBuilder().resolveTemplates(templateValues, encodeSlashInPath), this); + } + } + + @Override + public JerseyWebTarget resolveTemplatesFromEncoded(Map<String, Object> templateValues) + throws NullPointerException { + checkNotClosed(); + checkTemplateValues(templateValues); + + if (templateValues.isEmpty()) { + return this; + } else { + return new JerseyWebTarget(getUriBuilder().resolveTemplatesFromEncoded(templateValues), this); + } + } + + /** + * Check template values for {@code null} values. Throws {@code NullPointerException} if the name-value map or any of the + * names or encoded values in the map is {@code null}. + * + * @param templateValues map to check. + * @throws NullPointerException if the name-value map or any of the names or encoded values in the map + * is {@code null}. + */ + private void checkTemplateValues(final Map<String, Object> templateValues) throws NullPointerException { + Preconditions.checkNotNull(templateValues, "templateValues is 'null'."); + + for (final Map.Entry entry : templateValues.entrySet()) { + Preconditions.checkNotNull(entry.getKey(), "name is 'null'."); + Preconditions.checkNotNull(entry.getValue(), "value is 'null'."); + } + } + + @Override + public JerseyWebTarget register(Class<?> providerClass) { + checkNotClosed(); + config.register(providerClass); + return this; + } + + @Override + public JerseyWebTarget register(Object provider) { + checkNotClosed(); + config.register(provider); + return this; + } + + @Override + public JerseyWebTarget register(Class<?> providerClass, int bindingPriority) { + checkNotClosed(); + config.register(providerClass, bindingPriority); + return this; + } + + @Override + public JerseyWebTarget register(Class<?> providerClass, Class<?>... contracts) { + checkNotClosed(); + config.register(providerClass, contracts); + return this; + } + + @Override + public JerseyWebTarget register(Class<?> providerClass, Map<Class<?>, Integer> contracts) { + checkNotClosed(); + config.register(providerClass, contracts); + return this; + } + + @Override + public JerseyWebTarget register(Object provider, int bindingPriority) { + checkNotClosed(); + config.register(provider, bindingPriority); + return this; + } + + @Override + public JerseyWebTarget register(Object provider, Class<?>... contracts) { + checkNotClosed(); + config.register(provider, contracts); + return this; + } + + @Override + public JerseyWebTarget register(Object provider, Map<Class<?>, Integer> contracts) { + checkNotClosed(); + config.register(provider, contracts); + return this; + } + + @Override + public JerseyWebTarget property(String name, Object value) { + checkNotClosed(); + config.property(name, value); + return this; + } + + @Override + public ClientConfig getConfiguration() { + checkNotClosed(); + return config.getConfiguration(); + } + + @Override + public JerseyWebTarget preInitialize() { + config.preInitialize(); + return this; + } + + @Override + public String toString() { + return "JerseyWebTarget { " + targetUri.toTemplate() + " }"; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/RequestEntityProcessing.java b/core-client/src/main/java/org/glassfish/jersey/client/RequestEntityProcessing.java new file mode 100644 index 0000000..34d1ac8 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/RequestEntityProcessing.java
@@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +/** + * Defines values for the {@link ClientProperties#REQUEST_ENTITY_PROCESSING} property. + * + * @author Miroslav Fuksa + */ +public enum RequestEntityProcessing { + /** + * Request entity will be buffered in the memory in order to determine content length that + * will be send as a Content-Length header in the request. + */ + BUFFERED, + + /** + * Entity will be send as chunked encoded (no Content-length is specified, entity is streamed). + */ + CHUNKED + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java b/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java new file mode 100644 index 0000000..99de792 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/RequestProcessingInitializationStage.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Collections; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.WriterInterceptor; + +import javax.inject.Provider; + +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.message.MessageBodyWorkers; +import org.glassfish.jersey.model.internal.RankedComparator; + +/** + * Function that can be put to an acceptor chain to properly initialize + * the client-side request-scoped processing injection for the current + * request and response exchange. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class RequestProcessingInitializationStage implements Function<ClientRequest, ClientRequest> { + private final Provider<Ref<ClientRequest>> requestRefProvider; + private final MessageBodyWorkers workersProvider; + private final Iterable<WriterInterceptor> writerInterceptors; + private final Iterable<ReaderInterceptor> readerInterceptors; + + /** + * Create new {@link org.glassfish.jersey.message.MessageBodyWorkers} initialization function + * for requests and responses. + * + * @param requestRefProvider client request context reference injection provider. + * @param workersProvider message body workers injection provider. + * @param injectionManager injection manager. + */ + public RequestProcessingInitializationStage( + Provider<Ref<ClientRequest>> requestRefProvider, + MessageBodyWorkers workersProvider, + InjectionManager injectionManager) { + this.requestRefProvider = requestRefProvider; + this.workersProvider = workersProvider; + writerInterceptors = Collections.unmodifiableList( + StreamSupport.stream( + Providers.getAllProviders(injectionManager, WriterInterceptor.class, + new RankedComparator<>()).spliterator(), false) + .collect(Collectors.toList()) + ); + readerInterceptors = Collections.unmodifiableList( + StreamSupport.stream( + Providers.getAllProviders(injectionManager, ReaderInterceptor.class, + new RankedComparator<>()).spliterator(), false) + .collect(Collectors.toList()) + ); + } + + @Override + public ClientRequest apply(ClientRequest requestContext) { + requestRefProvider.get().set(requestContext); + requestContext.setWorkers(workersProvider); + requestContext.setWriterInterceptors(writerInterceptors); + requestContext.setReaderInterceptors(readerInterceptors); + + return requestContext; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ResponseCallback.java b/core-client/src/main/java/org/glassfish/jersey/client/ResponseCallback.java new file mode 100644 index 0000000..6ed9064 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/ResponseCallback.java
@@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import javax.ws.rs.ProcessingException; + +import org.glassfish.jersey.process.internal.RequestScope; + +/** + * Client response processing callback. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +interface ResponseCallback { + + /** + * Called when the client invocation was successfully completed with a response. + * + * @param response response data. + * @param scope request processing scope. + */ + public void completed(ClientResponse response, RequestScope scope); + + /** + * Called when the invocation has failed for any reason. + * + * @param error contains failure details. + */ + public void failed(ProcessingException error); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java new file mode 100644 index 0000000..f526307 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
@@ -0,0 +1,106 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.core.HttpHeaders; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.util.Base64; + +/** + * Implementation of Basic Http Authentication method (RFC 2617). + * + * @author Miroslav Fuksa + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Craig McClanahan + */ +final class BasicAuthenticator { + + private final HttpAuthenticationFilter.Credentials defaultCredentials; + + /** + * Creates a new instance of basic authenticator. + * + * @param defaultCredentials Credentials. Can be {@code null} if no default credentials should be + * used. + */ + BasicAuthenticator(HttpAuthenticationFilter.Credentials defaultCredentials) { + this.defaultCredentials = defaultCredentials; + } + + private String calculateAuthentication(HttpAuthenticationFilter.Credentials credentials) { + String username = credentials.getUsername(); + byte[] password = credentials.getPassword(); + if (username == null) { + username = ""; + } + + if (password == null) { + password = new byte[0]; + } + + final byte[] prefix = (username + ":").getBytes(HttpAuthenticationFilter.CHARACTER_SET); + final byte[] usernamePassword = new byte[prefix.length + password.length]; + + System.arraycopy(prefix, 0, usernamePassword, 0, prefix.length); + System.arraycopy(password, 0, usernamePassword, prefix.length, password.length); + + return "Basic " + Base64.encodeAsString(usernamePassword); + } + + /** + * Adds authentication information to the request. + * + * @param request Request context. + * @throws RequestAuthenticationException in case that basic credentials missing or are in invalid format + */ + public void filterRequest(ClientRequestContext request) throws RequestAuthenticationException { + HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter.getCredentials(request, + defaultCredentials, HttpAuthenticationFilter.Type.BASIC); + if (credentials == null) { + throw new RequestAuthenticationException(LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_BASIC()); + } + request.getHeaders().add(HttpHeaders.AUTHORIZATION, calculateAuthentication(credentials)); + } + + /** + * Checks the response and if basic authentication is required then performs a new request + * with basic authentication. + * + * @param request Request context. + * @param response Response context (will be updated with newest response data if the request was repeated). + * @return {@code true} if response does not require authentication or if authentication is required, + * new request was done with digest authentication information and authentication was successful. + * @throws ResponseAuthenticationException in case that basic credentials missing or are in invalid format + */ + public boolean filterResponseAndAuthenticate(ClientRequestContext request, ClientResponseContext response) { + final String authenticate = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE); + if (authenticate != null && authenticate.trim().toUpperCase().startsWith("BASIC")) { + HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter + .getCredentials(request, defaultCredentials, HttpAuthenticationFilter.Type.BASIC); + + if (credentials == null) { + throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_BASIC()); + } + + return HttpAuthenticationFilter.repeatRequest(request, response, calculateAuthentication(credentials)); + } + return false; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/DigestAuthenticator.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/DigestAuthenticator.java new file mode 100644 index 0000000..59f75bd --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/DigestAuthenticator.java
@@ -0,0 +1,463 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import java.io.IOException; +import java.net.URI; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.message.MessageUtils; +import org.glassfish.jersey.uri.UriComponent; + +/** + * Implementation of Digest Http Authentication method (RFC 2617). + * + * @author raphael.jolivet@gmail.com + * @author Stefan Katerkamp (stefan@katerkamp.de) + * @author Miroslav Fuksa + */ +final class DigestAuthenticator { + + private static final char[] HEX_ARRAY = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final Pattern KEY_VALUE_PAIR_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|(\\w+))\\s*,?\\s*"); + private static final int CLIENT_NONCE_BYTE_COUNT = 4; + + private final SecureRandom randomGenerator; + private final HttpAuthenticationFilter.Credentials credentials; + + private final Map<URI, DigestScheme> digestCache; + + /** + * Create a new instance initialized from credentials and configuration. + * + * @param credentials Credentials. Can be {@code null} if there are no default credentials. + * @param limit Maximum number of URIs that should be kept in the cache containing URIs and their + * {@link org.glassfish.jersey.client.authentication.DigestAuthenticator.DigestScheme}. + */ + DigestAuthenticator(final HttpAuthenticationFilter.Credentials credentials, final int limit) { + this.credentials = credentials; + + digestCache = Collections.synchronizedMap(new LinkedHashMap<URI, DigestScheme>(limit) { + // use id as it is an anonymous inner class with changed behaviour + private static final long serialVersionUID = 2546245625L; + + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > limit; + } + }); + + try { + randomGenerator = SecureRandom.getInstance("SHA1PRNG"); + } catch (final NoSuchAlgorithmException e) { + throw new RequestAuthenticationException(LocalizationMessages.ERROR_DIGEST_FILTER_GENERATOR(), e); + } + } + + /** + * Process request and add authentication information if possible. + * + * @param request Request context. + * @return {@code true} if authentication information was added. + * @throws IOException When error with encryption occurs. + */ + boolean filterRequest(final ClientRequestContext request) throws IOException { + final DigestScheme digestScheme = digestCache.get(request.getUri()); + if (digestScheme != null) { + final HttpAuthenticationFilter.Credentials cred = HttpAuthenticationFilter.getCredentials(request, + this.credentials, HttpAuthenticationFilter.Type.DIGEST); + if (cred != null) { + request.getHeaders().add(HttpHeaders.AUTHORIZATION, createNextAuthToken(digestScheme, request, cred)); + return true; + } + } + return false; + } + + /** + * Process response and repeat the request if digest authentication is requested. When request is repeated + * the response will be modified to contain new response information. + * + * @param request Request context. + * @param response Response context (will be updated with newest response data if the request was repeated). + * @return {@code true} if response does not require authentication or if authentication is required, + * new request was done with digest authentication information and authentication was successful. + * @throws IOException When error with encryption occurs. + */ + public boolean filterResponse(final ClientRequestContext request, final ClientResponseContext response) throws IOException { + + if (Response.Status.fromStatusCode(response.getStatus()) == Response.Status.UNAUTHORIZED) { + + final DigestScheme digestScheme = parseAuthHeaders(response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE)); + if (digestScheme == null) { + return false; + } + + // assemble authentication request and resend it + final HttpAuthenticationFilter.Credentials cred = HttpAuthenticationFilter.getCredentials(request, + this.credentials, HttpAuthenticationFilter.Type.DIGEST); + if (cred == null) { + + throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_DIGEST()); + } + + final boolean success = HttpAuthenticationFilter.repeatRequest(request, response, createNextAuthToken(digestScheme, + request, cred)); + if (success) { + digestCache.put(request.getUri(), digestScheme); + } else { + digestCache.remove(request.getUri()); + } + return success; + } + return true; + } + + /** + * Parse digest header. + * + * @param headers List of header strings + * @return DigestScheme or {@code null} if no digest header exists. + */ + private DigestScheme parseAuthHeaders(final List<?> headers) throws IOException { + + if (headers == null) { + return null; + } + for (final Object lineObject : headers) { + + if (!(lineObject instanceof String)) { + continue; + } + final String line = (String) lineObject; + final String[] parts = line.trim().split("\\s+", 2); + + if (parts.length != 2) { + continue; + } + if (!"digest".equals(parts[0].toLowerCase())) { + continue; + } + + String realm = null; + String nonce = null; + String opaque = null; + QOP qop = QOP.UNSPECIFIED; + Algorithm algorithm = Algorithm.UNSPECIFIED; + boolean stale = false; + + final Matcher match = KEY_VALUE_PAIR_PATTERN.matcher(parts[1]); + while (match.find()) { + // expect 4 groups (key)=("(val)" | (val)) + final int nbGroups = match.groupCount(); + if (nbGroups != 4) { + continue; + } + final String key = match.group(1); + final String valNoQuotes = match.group(3); + final String valQuotes = match.group(4); + final String val = (valNoQuotes == null) ? valQuotes : valNoQuotes; + if ("qop".equals(key)) { + qop = QOP.parse(val); + } else if ("realm".equals(key)) { + realm = val; + } else if ("nonce".equals(key)) { + nonce = val; + } else if ("opaque".equals(key)) { + opaque = val; + } else if ("stale".equals(key)) { + stale = Boolean.parseBoolean(val); + } else if ("algorithm".equals(key)) { + algorithm = Algorithm.parse(val); + } + } + return new DigestScheme(realm, nonce, opaque, qop, algorithm, stale); + } + return null; + } + + /** + * Creates digest string including counter. + * + * @param ds DigestScheme instance + * @param requestContext client request context + * @return digest authentication token string + * @throws IOException + */ + private String createNextAuthToken(final DigestScheme ds, final ClientRequestContext requestContext, + final HttpAuthenticationFilter.Credentials credentials) throws IOException { + final StringBuilder sb = new StringBuilder(100); + sb.append("Digest "); + append(sb, "username", credentials.getUsername()); + append(sb, "realm", ds.getRealm()); + append(sb, "nonce", ds.getNonce()); + append(sb, "opaque", ds.getOpaque()); + append(sb, "algorithm", ds.getAlgorithm().toString(), false); + append(sb, "qop", ds.getQop().toString(), false); + + final String uri = UriComponent.fullRelativeUri(requestContext.getUri()); + append(sb, "uri", uri); + + final String ha1; + if (ds.getAlgorithm() == Algorithm.MD5_SESS) { + ha1 = md5(md5(credentials.getUsername(), ds.getRealm(), + new String(credentials.getPassword(), MessageUtils.getCharset(requestContext.getMediaType())))); + } else { + ha1 = md5(credentials.getUsername(), ds.getRealm(), + new String(credentials.getPassword(), MessageUtils.getCharset(requestContext.getMediaType()))); + } + + final String ha2 = md5(requestContext.getMethod(), uri); + + final String response; + if (ds.getQop() == QOP.UNSPECIFIED) { + response = md5(ha1, ds.getNonce(), ha2); + } else { + final String cnonce = randomBytes(CLIENT_NONCE_BYTE_COUNT); // client nonce + append(sb, "cnonce", cnonce); + final String nc = String.format("%08x", ds.incrementCounter()); // counter + append(sb, "nc", nc, false); + response = md5(ha1, ds.getNonce(), nc, cnonce, ds.getQop().toString(), ha2); + } + append(sb, "response", response); + + return sb.toString(); + } + + /** + * Append comma separated key=value token + * + * @param sb string builder instance + * @param key key string + * @param value value string + * @param useQuote true if value needs to be enclosed in quotes + */ + private static void append(final StringBuilder sb, final String key, final String value, final boolean useQuote) { + + if (value == null) { + return; + } + if (sb.length() > 0) { + if (sb.charAt(sb.length() - 1) != ' ') { + sb.append(','); + } + } + sb.append(key); + sb.append('='); + if (useQuote) { + sb.append('"'); + } + sb.append(value); + if (useQuote) { + sb.append('"'); + } + } + + /** + * Append comma separated key=value token. The value gets enclosed in + * quotes. + * + * @param sb string builder instance + * @param key key string + * @param value value string + */ + private static void append(final StringBuilder sb, final String key, final String value) { + append(sb, key, value, true); + } + + /** + * Convert bytes array to hex string. + * + * @param bytes array of bytes + * @return hex string + */ + private static String bytesToHex(final byte[] bytes) { + final char[] hexChars = new char[bytes.length * 2]; + int v; + for (int j = 0; j < bytes.length; j++) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Colon separated value MD5 hash. + * + * @param tokens one or more strings + * @return M5 hash string + * @throws IOException + */ + private static String md5(final String... tokens) throws IOException { + final StringBuilder sb = new StringBuilder(100); + for (final String token : tokens) { + if (sb.length() > 0) { + sb.append(':'); + } + sb.append(token); + } + + final MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (final NoSuchAlgorithmException ex) { + throw new IOException(ex.getMessage()); + } + md.update(sb.toString().getBytes(HttpAuthenticationFilter.CHARACTER_SET), 0, sb.length()); + final byte[] md5hash = md.digest(); + return bytesToHex(md5hash); + } + + /** + * Generate a random sequence of bytes and return its hex representation + * + * @param nbBytes number of bytes to generate + * @return hex string + */ + private String randomBytes(final int nbBytes) { + final byte[] bytes = new byte[nbBytes]; + randomGenerator.nextBytes(bytes); + return bytesToHex(bytes); + } + + private enum QOP { + + UNSPECIFIED(null), + AUTH("auth"); + + private final String qop; + + QOP(final String qop) { + this.qop = qop; + } + + @Override + public String toString() { + return qop; + } + + public static QOP parse(final String val) { + if (val == null || val.isEmpty()) { + return QOP.UNSPECIFIED; + } + if (val.contains("auth")) { + return QOP.AUTH; + } + throw new UnsupportedOperationException(LocalizationMessages.DIGEST_FILTER_QOP_UNSUPPORTED(val)); + } + } + + enum Algorithm { + + UNSPECIFIED(null), + MD5("MD5"), + MD5_SESS("MD5-sess"); + private final String md; + + Algorithm(final String md) { + this.md = md; + } + + @Override + public String toString() { + return md; + } + + public static Algorithm parse(String val) { + if (val == null || val.isEmpty()) { + return Algorithm.UNSPECIFIED; + } + val = val.trim(); + if (val.contains(MD5_SESS.md) || val.contains(MD5_SESS.md.toLowerCase())) { + return MD5_SESS; + } + return MD5; + } + } + + /** + * Digest scheme POJO + */ + final class DigestScheme { + + private final String realm; + private final String nonce; + private final String opaque; + private final Algorithm algorithm; + private final QOP qop; + private final boolean stale; + private volatile int nc; + + DigestScheme(final String realm, final String nonce, final String opaque, final QOP qop, final Algorithm algorithm, + final boolean stale) { + this.realm = realm; + this.nonce = nonce; + this.opaque = opaque; + this.qop = qop; + this.algorithm = algorithm; + this.stale = stale; + this.nc = 0; + } + + public int incrementCounter() { + return ++nc; + } + + public String getNonce() { + return nonce; + } + + public String getRealm() { + return realm; + } + + public String getOpaque() { + return opaque; + } + + public Algorithm getAlgorithm() { + return algorithm; + } + + public QOP getQop() { + return qop; + } + + public boolean isStale() { + return stale; + } + + public int getNc() { + return nc; + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFeature.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFeature.java new file mode 100644 index 0000000..ae1b2da --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFeature.java
@@ -0,0 +1,581 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +/** + * Features that provides Http Basic and Digest client authentication (based on RFC 2617). + * <p> + * The feature can work in following modes: + * <ul> + * <li><b>BASIC:</b> Basic preemptive authentication. In preemptive mode the authentication information + * is send always with each HTTP request. This mode is more usual than the following non-preemptive mode + * (if you require BASIC authentication you will probably use this preemptive mode). This mode must + * be combined with usage of SSL/TLS as the password is send only BASE64 encoded.</li> + * <li><i>BASIC NON-PREEMPTIVE:</i> Basic non-preemptive authentication. In non-preemptive mode the + * authentication information is added only when server refuses the request with {@code 401} status code and + * then the request is repeated with authentication information. This mode has negative impact on the performance. + * The advantage is that it does not send credentials when they are not needed. This mode must + * be combined with usage of SSL/TLS as the password is send only BASE64 encoded. + * <p/> + * Please note that when you use non-preemptive authentication, Jersey client will make 2 requests to a resource, + * which also means that all registered filters will be invoked twice. + * </li> + * <li><b>DIGEST:</b> Http digest authentication. Does not require usage of SSL/TLS.</li> + * <li><b>UNIVERSAL:</b> Combination of basic and digest authentication. The feature works in non-preemptive + * mode which means that it sends requests without authentication information. If {@code 401} status + * code is returned, the request is repeated and an appropriate authentication is used based on the + * authentication requested in the response (defined in {@code WWW-Authenticate} HTTP header. The feature + * remembers which authentication requests were successful for given URI and next time tries to preemptively + * authenticate against this URI with latest successful authentication method. + * </li> + * </ul> + * </p> + * <p> + * To initialize the feature use static method of this feature. + * </p> + * <p> + * Example of building the feature in + * Basic authentication mode: + * <pre> + * HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("user", "superSecretPassword"); + * </pre> + * </p> + * <p> + * Example of building the feature in basic non-preemptive mode: + * <pre> + * HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder() + * .nonPreemptive().credentials("user", "superSecretPassword").build(); + * </pre> + * </p> + * <p> + * Example of building the feature in universal mode: + * <pre> + * HttpAuthenticationFeature feature = HttpAuthenticationFeature.universal("user", "superSecretPassword"); + * </pre> + * </p> + * <p> + * Example of building the feature in universal mode with different credentials for basic and digest: + * <pre> + * HttpAuthenticationFeature feature = HttpAuthenticationFeature.universalBuilder() + * .credentialsForBasic("user", "123456") + * .credentials("adminuser", "hello") + * .build(); + * </pre> + * </p> + * Example of building the feature in basic preemptive mode with no default credentials. Credentials will have + * to be supplied with each request using request properties (see below): + * <pre> + * HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder().build(); + * </pre> + * </p> + * <p> + * Once the feature is built it needs to be registered into the {@link javax.ws.rs.client.Client}, + * {@link javax.ws.rs.client.WebTarget} or other client configurable object. Example: + * <pre> + * final Client client = ClientBuilder.newClient(); + * client.register(feature); + * </pre> + * </p> + * + * Then you invoke requests as usual and authentication will be handled by the feature. + * You can change the credentials for each request using properties + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_USERNAME} and + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_PASSWORD}. Example: + * <pre> + * final Response response = client.target("http://localhost:8080/rest/homer/contact").request() + * .property(HTTP_AUTHENTICATION_BASIC_USERNAME, "homer") + * .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, "p1swd745").get(); + * </pre> + * <p> + * This class also contains property key definitions for overriding only specific basic or digest credentials: + * <ul> + * <li> + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_BASIC_USERNAME} and + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_BASIC_PASSWORD} + * </li> + * <li> + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_DIGEST_USERNAME} and + * {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature#HTTP_AUTHENTICATION_DIGEST_PASSWORD}. + * </li> + * </ul> + * </p> + * + * @author Miroslav Fuksa + * + * @since 2.5 + */ +public class HttpAuthenticationFeature implements Feature { + + /** + * Feature authentication mode. + */ + static enum Mode { + /** + * Basic preemptive. + **/ + BASIC_PREEMPTIVE, + /** + * Basic non preemptive + */ + BASIC_NON_PREEMPTIVE, + /** + * Digest. + */ + DIGEST, + /** + * Universal. + */ + UNIVERSAL + } + + /** + * Builder that creates instances of {@link HttpAuthenticationFeature}. + */ + public static interface Builder { + + /** + * Set credentials. + * + * @param username Username. + * @param password Password as byte array. + * @return This builder. + */ + public Builder credentials(String username, byte[] password); + + /** + * Set credentials. + * + * @param username Username. + * @param password Password as {@link String}. + * @return This builder. + */ + public Builder credentials(String username, String password); + + /** + * Build the feature. + * + * @return Http authentication feature configured from this builder. + */ + public HttpAuthenticationFeature build(); + } + + /** + * Extension of {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature.Builder} + * that builds the http authentication feature configured for basic authentication. + */ + public static interface BasicBuilder extends Builder { + + /** + * Configure the builder to create features in non-preemptive basic authentication mode. + * + * @return This builder. + */ + public BasicBuilder nonPreemptive(); + } + + /** + * Extension of {@link org.glassfish.jersey.client.authentication.HttpAuthenticationFeature.Builder} + * that builds the http authentication feature configured in universal mode that supports + * basic and digest authentication. + */ + public static interface UniversalBuilder extends Builder { + + /** + * Set credentials that will be used for basic authentication only. + * + * @param username Username. + * @param password Password as {@link String}. + * @return This builder. + */ + public UniversalBuilder credentialsForBasic(String username, String password); + + /** + * Set credentials that will be used for basic authentication only. + * + * @param username Username. + * @param password Password as {@code byte array}. + * @return This builder. + */ + public UniversalBuilder credentialsForBasic(String username, byte[] password); + + /** + * Set credentials that will be used for digest authentication only. + * + * @param username Username. + * @param password Password as {@link String}. + * @return This builder. + */ + public UniversalBuilder credentialsForDigest(String username, String password); + + /** + * Set credentials that will be used for digest authentication only. + * + * @param username Username. + * @param password Password as {@code byte array}. + * @return This builder. + */ + public UniversalBuilder credentialsForDigest(String username, byte[] password); + } + + /** + * Implementation of all authentication builders. + */ + static class BuilderImpl implements UniversalBuilder, BasicBuilder { + + private String usernameBasic; + private byte[] passwordBasic; + private String usernameDigest; + private byte[] passwordDigest; + private Mode mode; + + /** + * Create a new builder. + * + * @param mode Mode in which the final authentication feature should work. + */ + public BuilderImpl(Mode mode) { + this.mode = mode; + } + + @Override + public Builder credentials(String username, String password) { + return credentials(username, password == null ? null : password.getBytes(HttpAuthenticationFilter.CHARACTER_SET)); + } + + @Override + public Builder credentials(String username, byte[] password) { + credentialsForBasic(username, password); + credentialsForDigest(username, password); + return this; + } + + @Override + public UniversalBuilder credentialsForBasic(String username, String password) { + return credentialsForBasic(username, + password == null ? null : password.getBytes(HttpAuthenticationFilter.CHARACTER_SET)); + } + + @Override + public UniversalBuilder credentialsForBasic(String username, byte[] password) { + this.usernameBasic = username; + this.passwordBasic = password; + return this; + } + + @Override + public UniversalBuilder credentialsForDigest(String username, String password) { + return credentialsForDigest(username, + password == null ? null : password.getBytes(HttpAuthenticationFilter.CHARACTER_SET)); + } + + @Override + public UniversalBuilder credentialsForDigest(String username, byte[] password) { + this.usernameDigest = username; + this.passwordDigest = password; + return this; + } + + @Override + public HttpAuthenticationFeature build() { + return new HttpAuthenticationFeature(mode, + usernameBasic == null ? null + : new HttpAuthenticationFilter.Credentials(usernameBasic, passwordBasic), + usernameDigest == null ? null + : new HttpAuthenticationFilter.Credentials(usernameDigest, passwordDigest)); + } + + @Override + public BasicBuilder nonPreemptive() { + if (mode == Mode.BASIC_PREEMPTIVE) { + this.mode = Mode.BASIC_NON_PREEMPTIVE; + } + return this; + } + } + + /** + * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request} + * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override + * the username for http authentication feature for the request. + * <p> + * Example: + * <pre> + * Response response = client.target("http://localhost:8080/rest/joe/orders").request() + * .property(HTTP_AUTHENTICATION_USERNAME, "joe") + * .property(HTTP_AUTHENTICATION_PASSWORD, "p1swd745").get(); + * </pre> + * </p> + * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property + * (as shown in the example). This property pair overrides all password settings of the authentication + * feature for the current request. + * <p> + * The default value must be instance of {@link String}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String HTTP_AUTHENTICATION_USERNAME = "jersey.config.client.http.auth.username"; + /** + * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request} + * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override + * the password for http authentication feature for the request. + * <p> + * Example: + * <pre> + * Response response = client.target("http://localhost:8080/rest/joe/orders").request() + * .property(HTTP_AUTHENTICATION_USERNAME, "joe") + * .property(HTTP_AUTHENTICATION_PASSWORD, "p1swd745").get(); + * </pre> + * </p> + * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_USERNAME} property + * (as shown in the example). This property pair overrides all password settings of the authentication + * feature for the current request. + * <p> + * The value must be instance of {@link String} or {@code byte} array ({@code byte[]}). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String HTTP_AUTHENTICATION_PASSWORD = "jersey.config.client.http.auth.password"; + + /** + * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request} + * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override + * the username for http basic authentication feature for the request. + * <p> + * Example: + * <pre> + * Response response = client.target("http://localhost:8080/rest/joe/orders").request() + * .property(HTTP_AUTHENTICATION_BASIC_USERNAME, "joe") + * .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, "p1swd745").get(); + * </pre> + * </p> + * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property + * (as shown in the example). The property pair influence only credentials used during basic authentication. + * + * <p> + * The value must be instance of {@link String}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + + */ + public static final String HTTP_AUTHENTICATION_BASIC_USERNAME = "jersey.config.client.http.auth.basic.username"; + + /** + * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request} + * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override + * the password for http basic authentication feature for the request. + * <p> + * Example: + * <pre> + * Response response = client.target("http://localhost:8080/rest/joe/orders").request() + * .property(HTTP_AUTHENTICATION_BASIC_USERNAME, "joe") + * .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, "p1swd745").get(); + * </pre> + * </p> + * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_USERNAME} property + * (as shown in the example). The property pair influence only credentials used during basic authentication. + * <p> + * The value must be instance of {@link String} or {@code byte} array ({@code byte[]}). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String HTTP_AUTHENTICATION_BASIC_PASSWORD = "jersey.config.client.http.auth.basic.password"; + + /** + * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request} + * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override + * the username for http digest authentication feature for the request. + * <p> + * Example: + * <pre> + * Response response = client.target("http://localhost:8080/rest/joe/orders").request() + * .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, "joe") + * .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, "p1swd745").get(); + * </pre> + * </p> + * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property + * (as shown in the example). The property pair influence only credentials used during digest authentication. + * <p> + * The value must be instance of {@link String}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String HTTP_AUTHENTICATION_DIGEST_USERNAME = "jersey.config.client.http.auth.digest.username"; + + /** + * Key of the property that can be set into the {@link javax.ws.rs.client.ClientRequestContext client request} + * using {@link javax.ws.rs.client.ClientRequestContext#setProperty(String, Object)} in order to override + * the password for http digest authentication feature for the request. + * <p> + * Example: + * <pre> + * Response response = client.target("http://localhost:8080/rest/joe/orders").request() + * .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, "joe") + * .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, "p1swd745").get(); + * </pre> + * </p> + * The property must be always combined with configuration of {@link #HTTP_AUTHENTICATION_PASSWORD} property + * (as shown in the example). The property pair influence only credentials used during digest authentication. + * <p> + * The value must be instance of {@link String} or {@code byte} array ({@code byte[]}). + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String HTTP_AUTHENTICATION_DIGEST_PASSWORD = "jersey.config.client.http.auth.digest.password"; + + /** + * Create the builder of the http authentication feature working in basic authentication mode. The builder + * can build preemptive and non-preemptive basic authentication features. + * + * @return Basic http authentication builder. + */ + public static BasicBuilder basicBuilder() { + return new BuilderImpl(Mode.BASIC_PREEMPTIVE); + } + + /** + * Create the http authentication feature in basic preemptive authentication mode initialized with credentials. + * + * @param username Username. + * @param password Password as {@code byte array}. + * @return Http authentication feature configured in basic mode. + */ + public static HttpAuthenticationFeature basic(String username, byte[] password) { + return build(Mode.BASIC_PREEMPTIVE, username, password); + } + + /** + * Create the http authentication feature in basic preemptive authentication mode initialized with credentials. + * + * @param username Username. + * @param password Password as {@link String}. + * @return Http authentication feature configured in basic mode. + */ + public static HttpAuthenticationFeature basic(String username, String password) { + return build(Mode.BASIC_PREEMPTIVE, username, password); + } + + /** + * Create the http authentication feature in digest authentication mode initialized without default + * credentials. Credentials will have to be supplied using request properties for each request. + * + * @return Http authentication feature configured in digest mode. + */ + public static HttpAuthenticationFeature digest() { + return build(Mode.DIGEST); + } + + /** + * Create the http authentication feature in digest authentication mode initialized with credentials. + * + * @param username Username. + * @param password Password as {@code byte array}. + * @return Http authentication feature configured in digest mode. + */ + public static HttpAuthenticationFeature digest(String username, byte[] password) { + return build(Mode.DIGEST, username, password); + } + + /** + * Create the http authentication feature in digest authentication mode initialized with credentials. + * + * @param username Username. + * @param password Password as {@link String}. + * @return Http authentication feature configured in digest mode. + */ + public static HttpAuthenticationFeature digest(String username, String password) { + return build(Mode.DIGEST, username, password); + } + + /** + * Create the builder that builds http authentication feature in combined mode supporting both, + * basic and digest authentication. + * + * @return Universal builder. + */ + public static UniversalBuilder universalBuilder() { + return new BuilderImpl(Mode.UNIVERSAL); + } + + /** + * Create the http authentication feature in combined mode supporting both, + * basic and digest authentication. + * + * @param username Username. + * @param password Password as {@code byte array}. + * @return Http authentication feature configured in digest mode. + */ + public static HttpAuthenticationFeature universal(String username, byte[] password) { + return build(Mode.UNIVERSAL, username, password); + } + + /** + * Create the http authentication feature in combined mode supporting both, + * basic and digest authentication. + * + * @param username Username. + * @param password Password as {@link String}. + * @return Http authentication feature configured in digest mode. + */ + public static HttpAuthenticationFeature universal(String username, String password) { + return build(Mode.UNIVERSAL, username, password); + } + + private static HttpAuthenticationFeature build(Mode mode) { + return new BuilderImpl(mode).build(); + } + + private static HttpAuthenticationFeature build(Mode mode, String username, byte[] password) { + return new BuilderImpl(mode).credentials(username, password).build(); + } + + private static HttpAuthenticationFeature build(Mode mode, String username, String password) { + return new BuilderImpl(mode).credentials(username, password).build(); + } + + private final Mode mode; + private final HttpAuthenticationFilter.Credentials basicCredentials; + private final HttpAuthenticationFilter.Credentials digestCredentials; + + private HttpAuthenticationFeature(Mode mode, HttpAuthenticationFilter.Credentials basicCredentials, + HttpAuthenticationFilter.Credentials digestCredentials) { + this.mode = mode; + this.basicCredentials = basicCredentials; + + this.digestCredentials = digestCredentials; + } + + @Override + public boolean configure(FeatureContext context) { + context.register(new HttpAuthenticationFilter(mode, basicCredentials, digestCredentials, context.getConfiguration())); + return true; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java new file mode 100644 index 0000000..97675c3 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java
@@ -0,0 +1,422 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.Priorities; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import javax.annotation.Priority; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.internal.LocalizationMessages; + +/** + * Http Authentication filter that provides basic and digest authentication (based on RFC 2617). + * + * @author Miroslav Fuksa + */ +@Priority(Priorities.AUTHENTICATION) +class HttpAuthenticationFilter implements ClientRequestFilter, ClientResponseFilter { + + /** + * Authentication type. + */ + enum Type { + /** + * Basic authentication. + */ + BASIC, + /** + * Digest authentication. + */ + DIGEST + } + + private static final String REQUEST_PROPERTY_FILTER_REUSED = + "org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.reused"; + private static final String REQUEST_PROPERTY_OPERATION = + "org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.operation"; + + /** + * Encoding used for authentication calculations. + */ + static final Charset CHARACTER_SET = Charset.forName("iso-8859-1"); + + private final HttpAuthenticationFeature.Mode mode; + + /** + * Cache with {@code URI:HTTP-METHOD} keys and authentication type as values. Contains successful + * authentications already performed by the filter. + */ + private final Map<String, Type> uriCache; + + private final DigestAuthenticator digestAuth; + private final BasicAuthenticator basicAuth; + + private static final int MAXIMUM_DIGEST_CACHE_SIZE = 10000; + + /** + * Create a new filter instance. + * + * @param mode Mode. + * @param basicCredentials Basic credentials (can be {@code null} if this filter does not work in the + * basic mode or if no default credentials are defined). + * @param digestCredentials Digest credentials (can be {@code null} if this filter does not work in the + * digest mode or if no default credentials are defined). + * @param configuration Configuration (non-{@code null}). + */ + HttpAuthenticationFilter(HttpAuthenticationFeature.Mode mode, Credentials basicCredentials, + Credentials digestCredentials, Configuration configuration) { + int limit = getMaximumCacheLimit(configuration); + + final int uriCacheLimit = limit * 2; // 2 is chosen to estimate there will be two times URIs + // for basic and digest together than only digest + // (limit estimates digest max URI number) + + uriCache = Collections.synchronizedMap(new LinkedHashMap<String, Type>(uriCacheLimit) { + private static final long serialVersionUID = 1946245645415625L; + + @Override + protected boolean removeEldestEntry(Map.Entry<String, Type> eldest) { + return size() > uriCacheLimit; + } + }); + + this.mode = mode; + switch (mode) { + case BASIC_PREEMPTIVE: + case BASIC_NON_PREEMPTIVE: + this.basicAuth = new BasicAuthenticator(basicCredentials); + this.digestAuth = null; + break; + case DIGEST: + this.basicAuth = null; + this.digestAuth = new DigestAuthenticator(digestCredentials, limit); + break; + case UNIVERSAL: + this.basicAuth = new BasicAuthenticator(basicCredentials); + this.digestAuth = new DigestAuthenticator(digestCredentials, limit); + break; + default: + throw new IllegalStateException("Not implemented."); + } + } + + private int getMaximumCacheLimit(Configuration configuration) { + int limit = ClientProperties.getValue(configuration.getProperties(), + ClientProperties.DIGESTAUTH_URI_CACHE_SIZELIMIT, MAXIMUM_DIGEST_CACHE_SIZE); + if (limit < 1) { + limit = MAXIMUM_DIGEST_CACHE_SIZE; + } + return limit; + } + + @Override + public void filter(ClientRequestContext request) throws IOException { + if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) { + return; + } + + if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { + return; + } + + Type operation = null; + if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) { + basicAuth.filterRequest(request); + operation = Type.BASIC; + } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) { + // do nothing + } else if (mode == HttpAuthenticationFeature.Mode.DIGEST) { + if (digestAuth.filterRequest(request)) { + operation = Type.DIGEST; + } + } else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) { + + Type lastSuccessfulMethod = uriCache.get(getCacheKey(request)); + if (lastSuccessfulMethod != null) { + request.setProperty(REQUEST_PROPERTY_OPERATION, lastSuccessfulMethod); + if (lastSuccessfulMethod == Type.BASIC) { + basicAuth.filterRequest(request); + operation = Type.BASIC; + } else if (lastSuccessfulMethod == Type.DIGEST) { + if (digestAuth.filterRequest(request)) { + operation = Type.DIGEST; + } + } + } + } + + if (operation != null) { + request.setProperty(REQUEST_PROPERTY_OPERATION, operation); + } + } + + @Override + public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException { + if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) { + return; + } + + Type result = null; // which authentication is requested: BASIC or DIGEST + boolean authenticate; + + if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) { + String authString = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE); + if (authString != null) { + final String upperCaseAuth = authString.trim().toUpperCase(); + if (upperCaseAuth.startsWith("BASIC")) { + result = Type.BASIC; + } else if (upperCaseAuth.startsWith("DIGEST")) { + result = Type.DIGEST; + } else { + // unknown authentication -> this filter cannot authenticate with this method + return; + } + } + authenticate = true; + } else { + authenticate = false; + } + + if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) { + // do nothing -> 401 will be returned to the client + } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) { + if (authenticate && result == Type.BASIC) { + basicAuth.filterResponseAndAuthenticate(request, response); + } + } else if (mode == HttpAuthenticationFeature.Mode.DIGEST) { + if (authenticate && result == Type.DIGEST) { + digestAuth.filterResponse(request, response); + } + } else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) { + Type operation = (Type) request.getProperty(REQUEST_PROPERTY_OPERATION); + if (operation != null) { + updateCache(request, !authenticate, operation); + } + + if (authenticate) { + boolean success = false; + + // now we have the challenge response and we can authenticate + if (result == Type.BASIC) { + success = basicAuth.filterResponseAndAuthenticate(request, response); + } else if (result == Type.DIGEST) { + success = digestAuth.filterResponse(request, response); + } + updateCache(request, success, result); + } + } + } + + private String getCacheKey(ClientRequestContext request) { + return request.getUri().toString() + ":" + request.getMethod(); + } + + private void updateCache(ClientRequestContext request, boolean success, Type operation) { + String cacheKey = getCacheKey(request); + if (success) { + this.uriCache.put(cacheKey, operation); + } else { + this.uriCache.remove(cacheKey); + } + } + + /** + * Repeat the {@code request} with provided {@code newAuthorizationHeader} + * and update the {@code response} with newest response data. + * + * @param request Request context. + * @param response Response context (will be updated with the new response data). + * @param newAuthorizationHeader {@code Authorization} header that should be added to the new request. + * @return {@code true} is the authentication was successful ({@code true} if 401 response code was not returned; + * {@code false} otherwise). + */ + static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader) { + Client client = request.getClient(); + + String method = request.getMethod(); + MediaType mediaType = request.getMediaType(); + URI lUri = request.getUri(); + + WebTarget resourceTarget = client.target(lUri); + + Invocation.Builder builder = resourceTarget.request(mediaType); + + MultivaluedMap<String, Object> newHeaders = new MultivaluedHashMap<String, Object>(); + + for (Map.Entry<String, List<Object>> entry : request.getHeaders().entrySet()) { + if (HttpHeaders.AUTHORIZATION.equals(entry.getKey())) { + continue; + } + newHeaders.put(entry.getKey(), entry.getValue()); + } + + newHeaders.add(HttpHeaders.AUTHORIZATION, newAuthorizationHeader); + builder.headers(newHeaders); + + builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true"); + + Invocation invocation; + if (request.getEntity() == null) { + invocation = builder.build(method); + } else { + invocation = builder.build(method, + Entity.entity(request.getEntity(), request.getMediaType())); + } + Response nextResponse = invocation.invoke(); + + if (nextResponse.hasEntity()) { + response.setEntityStream(nextResponse.readEntity(InputStream.class)); + } + MultivaluedMap<String, String> headers = response.getHeaders(); + headers.clear(); + headers.putAll(nextResponse.getStringHeaders()); + response.setStatus(nextResponse.getStatus()); + + return response.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode(); + } + + /** + * Credentials (username + password). + */ + static class Credentials { + + private final String username; + private final byte[] password; + + /** + * Create a new credentials from username and password as byte array. + * + * @param username Username. + * @param password Password as byte array. + */ + Credentials(String username, byte[] password) { + this.username = username; + this.password = password; + } + + /** + * Create a new credentials from username and password as {@link String}. + * + * @param username Username. + * @param password {@code String} password. + */ + Credentials(String username, String password) { + this.username = username; + this.password = password == null ? null : password.getBytes(CHARACTER_SET); + } + + /** + * Return username. + * + * @return username. + */ + String getUsername() { + return username; + } + + /** + * Return password as byte array. + * + * @return Password string in byte array representation. + */ + byte[] getPassword() { + return password; + } + } + + private static Credentials extractCredentials(ClientRequestContext request, Type type) { + String usernameKey = null; + String passwordKey = null; + if (type == null) { + usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_USERNAME; + passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_PASSWORD; + } else if (type == Type.BASIC) { + usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME; + passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD; + } else if (type == Type.DIGEST) { + usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_DIGEST_USERNAME; + passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_DIGEST_PASSWORD; + } + + String userName = (String) request.getProperty(usernameKey); + if (userName != null && !userName.equals("")) { + byte[] pwdBytes; + Object password = request.getProperty(passwordKey); + if (password instanceof byte[]) { + pwdBytes = ((byte[]) password); + } else if (password instanceof String) { + pwdBytes = ((String) password).getBytes(CHARACTER_SET); + } else { + throw new RequestAuthenticationException( + LocalizationMessages.AUTHENTICATION_CREDENTIALS_REQUEST_PASSWORD_UNSUPPORTED()); + } + return new Credentials(userName, pwdBytes); + } + return null; + } + + /** + * Get credentials actual for the current request. Priorities in credentials selection are the following: + * <ol> + * <li>Basic/digest specific credentials defined in the request properties</li> + * <li>Common credentials defined in the request properties</li> + * <li>{@code defaultCredentials}</li> + * </ol> + * + * @param request Request from which credentials should be extracted. + * @param defaultCredentials Default credentials (can be {@code null}). + * @param type Type of requested credentials. + * @return Credentials or {@code null} if no credentials are found and {@code defaultCredentials} are {@code null}. + * @throws RequestAuthenticationException in case the {@code username} or {@code password} is invalid. + */ + static Credentials getCredentials(ClientRequestContext request, Credentials defaultCredentials, Type type) { + Credentials commonCredentials = extractCredentials(request, type); + + if (commonCredentials != null) { + return commonCredentials; + } else { + Credentials specificCredentials = extractCredentials(request, null); + + return specificCredentials != null ? specificCredentials : defaultCredentials; + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/RequestAuthenticationException.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/RequestAuthenticationException.java new file mode 100644 index 0000000..4ca0ec2 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/RequestAuthenticationException.java
@@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import javax.ws.rs.ProcessingException; + +/** + * Exception thrown by security request authentication. + * + * @author Petr Bouda + */ +public class RequestAuthenticationException extends ProcessingException { + + /** + * Creates new instance of this exception with exception cause. + * + * @param cause Exception cause. + */ + public RequestAuthenticationException(Throwable cause) { + super(cause); + } + + /** + * Creates new instance of this exception with exception message. + * + * @param message Exception message. + */ + public RequestAuthenticationException(String message) { + super(message); + } + + /** + * Creates new instance of this exception with exception message and exception cause. + * + * @param message Exception message. + * @param cause Exception cause. + */ + public RequestAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/ResponseAuthenticationException.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/ResponseAuthenticationException.java new file mode 100644 index 0000000..b6e39bd --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/ResponseAuthenticationException.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import javax.ws.rs.client.ResponseProcessingException; +import javax.ws.rs.core.Response; + +/** + * Exception thrown by security response authentication. + * + * @author Petr Bouda + */ +public class ResponseAuthenticationException extends ResponseProcessingException { + + /** + * Creates new instance of this exception with exception cause. + * + * @param response the response instance for which the processing failed. + * @param cause Exception cause. + */ + public ResponseAuthenticationException(Response response, Throwable cause) { + super(response, cause); + } + + /** + * Creates new instance of this exception with exception message. + * + * @param response the response instance for which the processing failed. + * @param message Exception message. + */ + public ResponseAuthenticationException(Response response, String message) { + super(response, message); + } + + /** + * Creates new instance of this exception with exception message and exception cause. + * + * @param response the response instance for which the processing failed. + * @param message Exception message. + * @param cause Exception cause. + */ + public ResponseAuthenticationException(Response response, String message, Throwable cause) { + super(response, message, cause); + } + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/package-info.java new file mode 100644 index 0000000..8ae9ed9 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Provides core client authentication mechanisms. + */ +package org.glassfish.jersey.client.authentication;
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/filter/CsrfProtectionFilter.java b/core-client/src/main/java/org/glassfish/jersey/client/filter/CsrfProtectionFilter.java new file mode 100644 index 0000000..c76a27e --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/filter/CsrfProtectionFilter.java
@@ -0,0 +1,77 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; + +/** + * Simple client-side filter that adds X-Requested-By headers to all state-changing + * request (i.e. request for methods other than GET, HEAD and OPTIONS). + * This is to satisfy the requirements of the {@code org.glassfish.jersey.server.filter.CsrfProtectionFilter} + * on the server side. + * + * @author Martin Matula + */ +public class CsrfProtectionFilter implements ClientRequestFilter { + + /** + * Name of the header this filter will attach to the request. + */ + public static final String HEADER_NAME = "X-Requested-By"; + + private static final Set<String> METHODS_TO_IGNORE; + + static { + HashSet<String> mti = new HashSet<String>(); + mti.add("GET"); + mti.add("OPTIONS"); + mti.add("HEAD"); + METHODS_TO_IGNORE = Collections.unmodifiableSet(mti); + } + + private final String requestedBy; + + /** + * Creates a new instance of the filter with X-Requested-By header value set to empty string. + */ + public CsrfProtectionFilter() { + this(""); + } + + /** + * Initialized the filter with a desired value of the X-Requested-By header. + * + * @param requestedBy Desired value of X-Requested-By header the filter + * will be adding for all potentially state changing requests. + */ + public CsrfProtectionFilter(final String requestedBy) { + this.requestedBy = requestedBy; + } + + @Override + public void filter(ClientRequestContext rc) throws IOException { + if (!METHODS_TO_IGNORE.contains(rc.getMethod()) && !rc.getHeaders().containsKey(HEADER_NAME)) { + rc.getHeaders().add(HEADER_NAME, requestedBy); + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFeature.java b/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFeature.java new file mode 100644 index 0000000..d9a43c4 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFeature.java
@@ -0,0 +1,81 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.spi.ContentEncoder; + +/** + * Feature that configures support for content encodings on the client side. + * This feature registers {@link EncodingFilter} and the specified set of + * {@link org.glassfish.jersey.spi.ContentEncoder encoding providers} to the + * {@link javax.ws.rs.core.Configurable client configuration}. It also allows + * setting the value of {@link ClientProperties#USE_ENCODING} property. + * + * @author Martin Matula + */ +public class EncodingFeature implements Feature { + private final String useEncoding; + private final Class<?>[] encodingProviders; + + /** + * Create a new instance of the feature. + * + * @param encodingProviders Encoding providers to be registered in the client configuration. + */ + public EncodingFeature(Class<?>... encodingProviders) { + this(null, encodingProviders); + } + + /** + * Create a new instance of the feature specifying the default value for the + * {@link ClientProperties#USE_ENCODING} property. Unless the value is set in the client configuration + * properties at the time when this feature gets enabled, the provided value will be used. + * + * @param useEncoding Default value of {@link ClientProperties#USE_ENCODING} property. + * @param encoders Encoders to be registered in the client configuration. + */ + public EncodingFeature(String useEncoding, Class<?>... encoders) { + this.useEncoding = useEncoding; + + Providers.ensureContract(ContentEncoder.class, encoders); + this.encodingProviders = encoders; + } + + + @Override + public boolean configure(FeatureContext context) { + if (useEncoding != null) { + // properties take precedence over the constructor value + if (!context.getConfiguration().getProperties().containsKey(ClientProperties.USE_ENCODING)) { + context.property(ClientProperties.USE_ENCODING, useEncoding); + } + } + for (Class<?> provider : encodingProviders) { + context.register(provider); + } + boolean enable = useEncoding != null || encodingProviders.length > 0; + if (enable) { + context.register(EncodingFilter.class); + } + return enable; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java b/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java new file mode 100644 index 0000000..bd15ec4 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java
@@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Logger; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; + +import javax.inject.Inject; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.spi.ContentEncoder; + +/** + * Client filter adding support for {@link org.glassfish.jersey.spi.ContentEncoder content encoding}. The filter adds + * list of supported encodings to the Accept-Header values. + * Supported encodings are determined by looking + * up all the {@link org.glassfish.jersey.spi.ContentEncoder} implementations registered in the corresponding + * {@link ClientConfig client configuration}. + * <p> + * If {@link ClientProperties#USE_ENCODING} client property is set, the filter will add Content-Encoding header with + * the value of the property, unless Content-Encoding header has already been set. + * </p> + * + * @author Martin Matula + */ +public final class EncodingFilter implements ClientRequestFilter { + @Inject + private InjectionManager injectionManager; + private volatile List<Object> supportedEncodings = null; + + @Override + public void filter(ClientRequestContext request) throws IOException { + if (getSupportedEncodings().isEmpty()) { + return; + } + + request.getHeaders().addAll(HttpHeaders.ACCEPT_ENCODING, getSupportedEncodings()); + + String useEncoding = (String) request.getConfiguration().getProperty(ClientProperties.USE_ENCODING); + if (useEncoding != null) { + if (!getSupportedEncodings().contains(useEncoding)) { + Logger.getLogger(getClass().getName()).warning(LocalizationMessages.USE_ENCODING_IGNORED( + ClientProperties.USE_ENCODING, useEncoding, getSupportedEncodings())); + } else { + if (request.hasEntity()) { // don't add Content-Encoding header for requests with no entity + if (request.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING) == null) { + request.getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, useEncoding); + } + } + } + } + } + + List<Object> getSupportedEncodings() { + // no need for synchronization - in case of a race condition, the property + // may be set twice, but it does not break anything + if (supportedEncodings == null) { + SortedSet<String> se = new TreeSet<>(); + List<ContentEncoder> encoders = injectionManager.getAllInstances(ContentEncoder.class); + for (ContentEncoder encoder : encoders) { + se.addAll(encoder.getSupportedEncodings()); + } + supportedEncodings = new ArrayList<>(se); + } + return supportedEncodings; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/filter/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/filter/package-info.java new file mode 100644 index 0000000..0d2c4b3 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/filter/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Provides core client filters. + */ +package org.glassfish.jersey.client.filter;
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java new file mode 100644 index 0000000..65b2eae --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
@@ -0,0 +1,508 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.internal; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.UnsafeValue; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.message.internal.Statuses; + +/** + * Default client transport connector using {@link HttpURLConnection}. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class HttpUrlConnector implements Connector { + + private static final Logger LOGGER = Logger.getLogger(HttpUrlConnector.class.getName()); + private static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders"; + // The list of restricted headers is extracted from sun.net.www.protocol.http.HttpURLConnection + private static final String[] restrictedHeaders = { + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Connection", /* close is allowed */ + "Content-Length", + "Content-Transfer-Encoding", + "Host", + "Keep-Alive", + "Origin", + "Trailer", + "Transfer-Encoding", + "Upgrade", + "Via" + }; + + private static final Set<String> restrictedHeaderSet = new HashSet<>(restrictedHeaders.length); + + static { + for (String headerName : restrictedHeaders) { + restrictedHeaderSet.add(headerName.toLowerCase()); + } + } + + private final HttpUrlConnectorProvider.ConnectionFactory connectionFactory; + private final int chunkSize; + private final boolean fixLengthStreaming; + private final boolean setMethodWorkaround; + private final boolean isRestrictedHeaderPropertySet; + private final LazyValue<SSLSocketFactory> sslSocketFactory; + + /** + * Create new {@code HttpUrlConnector} instance. + * + * @param client JAX-RS client instance for which the connector is being created. + * @param connectionFactory {@link javax.net.ssl.HttpsURLConnection} factory to be used when creating connections. + * @param chunkSize chunk size to use when using HTTP chunked transfer coding. + * @param fixLengthStreaming specify if the the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) + * fixed-length streaming mode} on the underlying HTTP URL connection instances should be + * used when sending requests. + * @param setMethodWorkaround specify if the reflection workaround should be used to set HTTP URL connection method + * name. See {@link HttpUrlConnectorProvider#SET_METHOD_WORKAROUND} for details. + */ + public HttpUrlConnector( + final Client client, + final HttpUrlConnectorProvider.ConnectionFactory connectionFactory, + final int chunkSize, + final boolean fixLengthStreaming, + final boolean setMethodWorkaround) { + + sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() { + @Override + public SSLSocketFactory get() { + return client.getSslContext().getSocketFactory(); + } + }); + + this.connectionFactory = connectionFactory; + this.chunkSize = chunkSize; + this.fixLengthStreaming = fixLengthStreaming; + this.setMethodWorkaround = setMethodWorkaround; + + // check if sun.net.http.allowRestrictedHeaders system property has been set and log the result + // the property is being cached in the HttpURLConnection, so this is only informative - there might + // already be some connection(s), that existed before the property was set/changed. + isRestrictedHeaderPropertySet = Boolean.valueOf(AccessController.doPrivileged( + PropertiesHelper.getSystemProperty(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY, "false") + )); + + LOGGER.config(isRestrictedHeaderPropertySet + ? LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_TRUE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY) + : LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_FALSE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY) + ); + } + + private static InputStream getInputStream(final HttpURLConnection uc) throws IOException { + return new InputStream() { + private final UnsafeValue<InputStream, IOException> in = Values.lazy(new UnsafeValue<InputStream, IOException>() { + @Override + public InputStream get() throws IOException { + if (uc.getResponseCode() < Response.Status.BAD_REQUEST.getStatusCode()) { + return uc.getInputStream(); + } else { + InputStream ein = uc.getErrorStream(); + return (ein != null) ? ein : new ByteArrayInputStream(new byte[0]); + } + } + }); + + private volatile boolean closed = false; + + /** + * The motivation for this method is to straighten up a behaviour of {@link sun.net.www.http.KeepAliveStream} which + * is used here as a backing {@link InputStream}. The problem is that its access methods (e.g., {@link + * sun.net.www.http.KeepAliveStream#read()}) do not throw {@link IOException} if the stream is closed. This behaviour + * contradicts with {@link InputStream} contract. + * <p/> + * This is a part of fix of JERSEY-2878 + * <p/> + * Note that {@link java.io.FilterInputStream} also changes the contract of + * {@link java.io.FilterInputStream#read(byte[], int, int)} as it doesn't state that closed stream causes an {@link + * IOException} which might be questionable. Nevertheless, our contract is {@link InputStream} and as such, the + * stream we're offering must comply with it. + * + * @throws IOException when the stream is closed. + */ + private void throwIOExceptionIfClosed() throws IOException { + if (closed) { + throw new IOException("Stream closed"); + } + } + + @Override + public int read() throws IOException { + int result = in.get().read(); + throwIOExceptionIfClosed(); + return result; + } + + @Override + public int read(byte[] b) throws IOException { + int result = in.get().read(b); + throwIOExceptionIfClosed(); + return result; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result = in.get().read(b, off, len); + throwIOExceptionIfClosed(); + return result; + } + + @Override + public long skip(long n) throws IOException { + long result = in.get().skip(n); + throwIOExceptionIfClosed(); + return result; + } + + @Override + public int available() throws IOException { + int result = in.get().available(); + throwIOExceptionIfClosed(); + return result; + } + + @Override + public void close() throws IOException { + try { + in.get().close(); + } finally { + closed = true; + } + } + + @Override + public void mark(int readLimit) { + try { + in.get().mark(readLimit); + } catch (IOException e) { + throw new IllegalStateException("Unable to retrieve the underlying input stream.", e); + } + } + + @Override + public void reset() throws IOException { + in.get().reset(); + throwIOExceptionIfClosed(); + } + + @Override + public boolean markSupported() { + try { + return in.get().markSupported(); + } catch (IOException e) { + throw new IllegalStateException("Unable to retrieve the underlying input stream.", e); + } + } + }; + } + + @Override + public ClientResponse apply(ClientRequest request) { + try { + return _apply(request); + } catch (IOException ex) { + throw new ProcessingException(ex); + } + } + + @Override + public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) { + + try { + callback.response(_apply(request)); + } catch (IOException ex) { + callback.failure(new ProcessingException(ex)); + } catch (Throwable t) { + callback.failure(t); + } + + return CompletableFuture.completedFuture(null); + } + + @Override + public void close() { + // do nothing + } + + /** + * Secure connection if necessary. + * <p/> + * Provided implementation sets {@link HostnameVerifier} and {@link SSLSocketFactory} to give connection, if that + * is an instance of {@link HttpsURLConnection}. + * + * @param client client associated with this client runtime. + * @param uc http connection to be secured. + */ + protected void secureConnection(final JerseyClient client, final HttpURLConnection uc) { + if (uc instanceof HttpsURLConnection) { + HttpsURLConnection suc = (HttpsURLConnection) uc; + + final HostnameVerifier verifier = client.getHostnameVerifier(); + if (verifier != null) { + suc.setHostnameVerifier(verifier); + } + + if (HttpsURLConnection.getDefaultSSLSocketFactory() == suc.getSSLSocketFactory()) { + // indicates that the custom socket factory was not set + suc.setSSLSocketFactory(sslSocketFactory.get()); + } + } + } + + private ClientResponse _apply(final ClientRequest request) throws IOException { + final HttpURLConnection uc; + + uc = this.connectionFactory.getConnection(request.getUri().toURL()); + uc.setDoInput(true); + + final String httpMethod = request.getMethod(); + if (request.resolveProperty(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, setMethodWorkaround)) { + setRequestMethodViaJreBugWorkaround(uc, httpMethod); + } else { + uc.setRequestMethod(httpMethod); + } + + uc.setInstanceFollowRedirects(request.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true)); + + uc.setConnectTimeout(request.resolveProperty(ClientProperties.CONNECT_TIMEOUT, uc.getConnectTimeout())); + + uc.setReadTimeout(request.resolveProperty(ClientProperties.READ_TIMEOUT, uc.getReadTimeout())); + + secureConnection(request.getClient(), uc); + + final Object entity = request.getEntity(); + if (entity != null) { + RequestEntityProcessing entityProcessing = request.resolveProperty( + ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + + if (entityProcessing == null || entityProcessing != RequestEntityProcessing.BUFFERED) { + final long length = request.getLengthLong(); + if (fixLengthStreaming && length > 0) { + // uc.setFixedLengthStreamingMode(long) was introduced in JDK 1.7 and Jersey client supports 1.6+ + if ("1.6".equals(Runtime.class.getPackage().getSpecificationVersion())) { + uc.setFixedLengthStreamingMode(request.getLength()); + } else { + uc.setFixedLengthStreamingMode(length); + } + } else if (entityProcessing == RequestEntityProcessing.CHUNKED) { + uc.setChunkedStreamingMode(chunkSize); + } + } + uc.setDoOutput(true); + + if ("GET".equalsIgnoreCase(httpMethod)) { + final Logger logger = Logger.getLogger(HttpUrlConnector.class.getName()); + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, LocalizationMessages.HTTPURLCONNECTION_REPLACES_GET_WITH_ENTITY()); + } + } + + request.setStreamProvider(contentLength -> { + setOutboundHeaders(request.getStringHeaders(), uc); + return uc.getOutputStream(); + }); + request.writeEntity(); + + } else { + setOutboundHeaders(request.getStringHeaders(), uc); + } + + final int code = uc.getResponseCode(); + final String reasonPhrase = uc.getResponseMessage(); + final Response.StatusType status = + reasonPhrase == null ? Statuses.from(code) : Statuses.from(code, reasonPhrase); + final URI resolvedRequestUri; + try { + resolvedRequestUri = uc.getURL().toURI(); + } catch (URISyntaxException e) { + throw new ProcessingException(e); + } + + ClientResponse responseContext = new ClientResponse(status, request, resolvedRequestUri); + responseContext.headers( + uc.getHeaderFields() + .entrySet() + .stream() + .filter(stringListEntry -> stringListEntry.getKey() != null) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue)) + ); + responseContext.setEntityStream(getInputStream(uc)); + + return responseContext; + } + + private void setOutboundHeaders(MultivaluedMap<String, String> headers, HttpURLConnection uc) { + boolean restrictedSent = false; + for (Map.Entry<String, List<String>> header : headers.entrySet()) { + String headerName = header.getKey(); + String headerValue; + + List<String> headerValues = header.getValue(); + if (headerValues.size() == 1) { + headerValue = headerValues.get(0); + uc.setRequestProperty(headerName, headerValue); + } else { + StringBuilder b = new StringBuilder(); + boolean add = false; + for (Object value : headerValues) { + if (add) { + b.append(','); + } + add = true; + b.append(value); + } + headerValue = b.toString(); + uc.setRequestProperty(headerName, headerValue); + } + // if (at least one) restricted header was added and the allowRestrictedHeaders + if (!isRestrictedHeaderPropertySet && !restrictedSent) { + if (isHeaderRestricted(headerName, headerValue)) { + restrictedSent = true; + } + } + } + if (restrictedSent) { + LOGGER.warning(LocalizationMessages.RESTRICTED_HEADER_POSSIBLY_IGNORED(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)); + } + } + + private boolean isHeaderRestricted(String name, String value) { + name = name.toLowerCase(); + return name.startsWith("sec-") + || restrictedHeaderSet.contains(name) + && !("connection".equalsIgnoreCase(name) && "close".equalsIgnoreCase(value)); + } + + /** + * Workaround for a bug in {@code HttpURLConnection.setRequestMethod(String)} + * The implementation of Sun/Oracle is throwing a {@code ProtocolException} + * when the method is not in the list of the HTTP/1.1 default methods. + * This means that to use e.g. {@code PROPFIND} and others, we must apply this workaround. + * <p/> + * See issue http://java.net/jira/browse/JERSEY-639 + */ + private static void setRequestMethodViaJreBugWorkaround(final HttpURLConnection httpURLConnection, + final String method) { + try { + httpURLConnection.setRequestMethod(method); // Check whether we are running on a buggy JRE + } catch (final ProtocolException pe) { + try { + AccessController + .doPrivileged(new PrivilegedExceptionAction<Object>() { + @Override + public Object run() throws NoSuchFieldException, + IllegalAccessException { + try { + httpURLConnection.setRequestMethod(method); + // Check whether we are running on a buggy + // JRE + } catch (final ProtocolException pe) { + Class<?> connectionClass = httpURLConnection + .getClass(); + try { + final Field delegateField = connectionClass.getDeclaredField("delegate"); + delegateField.setAccessible(true); + + HttpURLConnection delegateConnection = + (HttpURLConnection) delegateField.get(httpURLConnection); + setRequestMethodViaJreBugWorkaround(delegateConnection, method); + } catch (NoSuchFieldException e) { + // Ignore for now, keep going + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(e); + } + try { + Field methodField; + while (connectionClass != null) { + try { + methodField = connectionClass + .getDeclaredField("method"); + } catch (NoSuchFieldException e) { + connectionClass = connectionClass + .getSuperclass(); + continue; + } + methodField.setAccessible(true); + methodField.set(httpURLConnection, method); + break; + } + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + return null; + } + }); + } catch (final PrivilegedActionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new RuntimeException(cause); + } + } + } + } + + @Override + public String getName() { + return "HttpUrlConnection " + AccessController.doPrivileged(PropertiesHelper.getSystemProperty("java.version")); + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/package-info.java new file mode 100644 index 0000000..a11dd05 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client-side classes. + */ +package org.glassfish.jersey.client;
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/AsyncConnectorCallback.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/AsyncConnectorCallback.java new file mode 100644 index 0000000..f4d5e8e --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/AsyncConnectorCallback.java
@@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.spi; + +import org.glassfish.jersey.client.ClientResponse; + +/** + * Asynchronous connector response processing callback contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public interface AsyncConnectorCallback { + + /** + * Invoked when a response for the asynchronously invoked request is available. + * + * @param response asynchronously received client response. + */ + public void response(ClientResponse response); + + /** + * Invoked in case the asynchronous request invocation failed. + * + * @param failure cause of the asynchronous request invocation failure. + */ + public void failure(Throwable failure); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/CachingConnectorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/CachingConnectorProvider.java new file mode 100644 index 0000000..d30de4c --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/CachingConnectorProvider.java
@@ -0,0 +1,60 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.spi; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +/** + * Caching connector provider. + * + * This utility provider can be used to serve as a lazily initialized provider of the same connector instance. + * <p> + * Note however that the connector instance will be configured using the runtime configuration of the first client instance that + * has invoked the {@link #getConnector(javax.ws.rs.client.Client, javax.ws.rs.core.Configuration)} method. + * {@link javax.ws.rs.client.Client} and {@link javax.ws.rs.core.Configuration} instance passed to subsequent + * {@code getConnector(...)} invocations will be ignored. As such, this connector provider should not be shared among client + * instances that have significantly different connector-specific settings. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.10 + */ +public class CachingConnectorProvider implements ConnectorProvider { + + private final ConnectorProvider delegate; + private Connector connector; + + + /** + * Create the caching connector provider. + * + * @param delegate delegate connector provider that will be used to initialize and create the connector instance which + * will be subsequently cached and reused. + */ + public CachingConnectorProvider(final ConnectorProvider delegate) { + this.delegate = delegate; + } + + @Override + public synchronized Connector getConnector(Client client, Configuration runtimeConfig) { + if (connector == null) { + connector = delegate.getConnector(client, runtimeConfig); + } + return connector; + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/Connector.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/Connector.java new file mode 100644 index 0000000..8c8953b --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/Connector.java
@@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.spi; + +import java.util.concurrent.Future; + +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.process.Inflector; + +/** + * Client transport connector extension contract. + * <p> + * Note that unlike most of the other {@link org.glassfish.jersey.spi.Contract Jersey SPI extension contracts}, + * Jersey {@code Connector} is not a typical runtime extension and as such cannot be directly registered + * using a configuration {@code register(...)} method. Jersey client runtime retrieves a {@code Connector} + * instance upon Jersey client runtime initialization using a {@link org.glassfish.jersey.client.spi.ConnectorProvider} + * registered in {@link org.glassfish.jersey.client.ClientConfig}. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +// Must not be annotated with @Contract +public interface Connector extends Inflector<ClientRequest, ClientResponse> { + /** + * Synchronously process client request into a response. + * + * The method is used by Jersey client runtime to synchronously send a request + * and receive a response. + * + * @param request Jersey client request to be sent. + * @return Jersey client response received for the client request. + * @throws javax.ws.rs.ProcessingException in case of any invocation failure. + */ + @Override + ClientResponse apply(ClientRequest request); + + /** + * Asynchronously process client request into a response. + * + * The method is used by Jersey client runtime to asynchronously send a request + * and receive a response. + * + * @param request Jersey client request to be sent. + * @param callback Jersey asynchronous connector callback to asynchronously receive + * the request processing result (either a response or a failure). + * @return asynchronously executed task handle. + */ + Future<?> apply(ClientRequest request, AsyncConnectorCallback callback); + + /** + * Get name of current connector. + * + * Should contain identification of underlying specification and optionally version number. + * Will be used in User-Agent header. + * + * @return name of current connector. Returning {@code null} or empty string means not including + * this information in a generated <tt>{@value javax.ws.rs.core.HttpHeaders#USER_AGENT}</tt> header. + */ + public String getName(); + + /** + * Close connector and release all it's internally associated resources. + */ + public void close(); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/ConnectorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/ConnectorProvider.java new file mode 100644 index 0000000..6a8b81a --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/ConnectorProvider.java
@@ -0,0 +1,63 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.spi; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +/** + * Jersey client connector provider contract. + * + * Connector provider is invoked by Jersey client runtime to provide a client connector + * to be used to send client requests over the wire to server-side resources. + * There can be only one connector provider registered in a single Jersey client instance. + * <p> + * Note that unlike most of the other {@link org.glassfish.jersey.spi.Contract Jersey SPI extension contracts}, + * {@code ConnectorProvider} is not a typical runtime extension and as such cannot be registered + * using a configuration {@code register(...)} method. Instead, it must be registered using via + * {@link org.glassfish.jersey.client.JerseyClientBuilder} using it's + * {@link org.glassfish.jersey.client.ClientConfig#connectorProvider(ConnectorProvider)} + * initializer method. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @since 2.5 + */ +// Must not be annotated with @Contract +public interface ConnectorProvider { + + /** + * Get a Jersey client connector instance for a given {@link Client client} instance + * and Jersey client runtime {@link Configuration configuration}. + * <p> + * Note that the supplied runtime configuration can be different from the client instance + * configuration as a single client can be used to serve multiple differently configured runtimes. + * While the {@link Client#getSslContext() SSL context} or {@link Client#getHostnameVerifier() hostname verifier} + * are shared, other configuration properties may change in each runtime. + * </p> + * <p> + * Based on the supplied client and runtime configuration data, it is up to each connector provider + * implementation to decide whether a new dedicated connector instance is required or if the existing, + * previously create connector instance can be reused. + * </p> + * + * @param client Jersey client instance. + * @param runtimeConfig Jersey client runtime configuration. + * @return configured {@link org.glassfish.jersey.client.spi.Connector} instance to be used by the client. + */ + public Connector getConnector(Client client, Configuration runtimeConfig); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/DefaultSslContextProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/DefaultSslContextProvider.java new file mode 100644 index 0000000..84dd5b4 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/DefaultSslContextProvider.java
@@ -0,0 +1,42 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.spi; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; + +/** + * Default {@link SSLContext} provider. + * <p> + * Can be used to override {@link SslConfigurator#getDefaultContext()}. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @since 2.21.1 + */ +public interface DefaultSslContextProvider { + + /** + * Get default {@code SSLContext}. + * <p> + * Returned instance is expected to be configured to container default values. + * + * @return default SSL context. + * @throws IllegalStateException when there is a problem with creating or obtaining default SSL context. + */ + SSLContext getDefaultSslContext(); +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/package-info.java new file mode 100644 index 0000000..fe2ed99 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/package-info.java
@@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client SPI classes/interfaces. + */ +package org.glassfish.jersey.client.spi;
diff --git a/core-client/src/main/resources/org/glassfish/jersey/client/internal/jdkconnector/localization.properties b/core-client/src/main/resources/org/glassfish/jersey/client/internal/jdkconnector/localization.properties new file mode 100644 index 0000000..e8f0342 --- /dev/null +++ b/core-client/src/main/resources/org/glassfish/jersey/client/internal/jdkconnector/localization.properties
@@ -0,0 +1,73 @@ +# +# Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +read.listener.set.only.once="Read listener can be set only once." +async.operation.not.supported="Operation not supported in synchronous mode." +sync.operation.not.supported="Operation not supported in asynchronous mode." +write.when.not.ready="Asynchronous write called when stream is in non-ready state." +stream.closed.for.input="This stream has already been closed for input." +write.listener.set.only.once="Write listener can be set only once." +stream.closed="The stream has been closed." +writing.failed="Writing data failed" +buffer.incorrect.length="Buffer passed for encoding is neither a multiple of chunkSize nor smaller than chunkSize." +connector.configuration="Connector configuration: {0}." +negative.chunk.size="Configured chunk size is negative: {0}, using default value: {1}." +timeout.receiving.response="Timeout receiving response." +timeout.receiving.response.body="Timeout receiving response body." +closed.while.sending.request="Connection closed by the server while sending request". +closed.while.receiving.response="Connection closed by the server while receiving response." +closed.while.receiving.body="Connection closed by the server while receiving response body." +connection.closed="Connection closed by the server." +closed.by.client.while.sending="Connection closed by the client while sending request." +closed.by.client.while.receiving="Connection closed by the client while receiving response." +closed.by.client.while.receiving.body="Connection closed by the client while receiving response body." +connection.timeout="Connection timed out." +connection.changing.state="HTTP connection {0}:{1} changing state {2} -> {3}." +unexpected.data.in.buffer="Unexpected data remain in the buffer after the HTTP response has been parsed." +http.initial.line.overflow="HTTP packet initial line is too large." +http.packet.header.overflow="HTTP packet header is too large." +http.negative.content.length="Content length cannot be less than 0." +http.invalid.content.length="Invalid format of content length code." +http.request.no.body="This HTTP request does not have a body." +http.request.no.buffered.body="Buffered body is available only in buffered body mode." +http.request.body.size.not.available="Body size is not available in chunked body mode." +proxy.user.name.missing="User name is missing" +proxy.password.missing="Password is missing" +proxy.qop.no.supported="The 'qop' (quality of protection) = {0} extension requested by the server is not supported. Cannot authenticate against the server using Http Digest Authentication." +proxy.407.twice="Received 407 for the second time." +proxy.fail.auth.header="Creating authorization header failed." +proxy.connect.fail="Connecting to proxy failed with status {0}." +proxy.missing.auth.header="Proxy-Authenticate header value is missing or empty." +proxy.unsupported.scheme="Unsupported scheme: {0}." +redirect.no.location="Received redirect that does not contain a location or the location is empty." +redirect.error.determining.location="Error determining redirect location." +redirect.infinite.loop="Infinite loop in chained redirects detected." +redirect.limit.reached="Max chained redirect limit ({0}) exceeded." +ssl.session.closed="SSL session has been closed." +http.body.size.overflow="Body size exceeds declared size" +http.invalid.chunk.size.hex.value="Invalid byte representing a hex value within a chunk length encountered : {0}" +http.unexpected.chunk.header="Unexpected HTTP chunk header." +http.chunk.encoding.prefix.overflow="The chunked encoding length prefix is too large." +http.trailer.header.overflow="The chunked encoding trailer header is too large." +transport.connection.not.closed="Could not close a connection." +transport.set.class.loader.failed="Cannot set thread context class loader." +transport.executor.closed="Cannot set thread context class loader." +transport.executor.queue.limit.reached="A limit of client thread pool queue has been reached." +thread.pool.max.size.too.small="Max thread pool size cannot be smaller than 3." +thread.pool.core.size.too.small="Core thread pool size cannot be smaller than 0." +http.connection.establishing.illegal.state="Cannot try to establish connection if the connection is in other than CREATED state\ + . Current state: {0}. +http.connection.not.idle="Http request cannot be sent over a connection that is in other state than IDLE. Current state: {0}"
diff --git a/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties b/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties new file mode 100644 index 0000000..12134f7 --- /dev/null +++ b/core-client/src/main/resources/org/glassfish/jersey/client/internal/localization.properties
@@ -0,0 +1,75 @@ +# +# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License v. 2.0, which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# This Source Code may also be made available under the following Secondary +# Licenses when the conditions for such availability set forth in the +# Eclipse Public License v. 2.0 are satisfied: GNU General Public License, +# version 2 with the GNU Classpath Exception, which is available at +# https://www.gnu.org/software/classpath/license.html. +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +# + +#brief.message.identifier=Message text, possibly with some attributes "{0}" etc. +authentication.credentials.missing.basic=Credentials must be defined for basic authentication. Define username and password either when creating HttpAuthenticationFeature or use specific credentials for each request using the request property (see HttpAuthenticationFeature). +authentication.credentials.missing.digest=Credentials must be defined for digest authentication. Define username and password either when creating HttpAuthenticationFeature or use specific credentials for each request using the request property (see HttpAuthenticationFeature). +authentication.credentials.request.password.unsupported=Unsupported password type class. Password passed in the request property must be String or byte[]. +chunked.input.closed=Chunked input has been closed already. +chunked.input.media.type.null=Specified chunk media type must not be null. +chunked.input.stream.closing.error=Error closing chunked input's underlying response input stream. +client.instance.closed=Client instance has been closed. +client.invocation.link.null=Link of the newly created invocation must not be null. +client.response.resolved.uri.null=Client response resolved URI must not be null. +client.response.resolved.uri.not.absolute=Client response resolved URI must be absolute. +client.response.status.null=Client response status must not be null. +client.rx.provider.null=RxInvokerProvider returned null. +client.rx.provider.not.registered=RxInvokerProvider for type {0} is not registered. +client.target.link.null=Link to the newly created target must not be null. +client.uri.template.null=URI template of the newly created target must not be null. +client.uri.null=URI of the newly created target must not be null. +client.uri.builder.null=URI builder of the newly created target must not be null. +digest.filter.qop.unsupported=The 'qop' (quality of protection) = {0} extension requested by the server is not supported by Jersey HttpDigestAuthFilter. Cannot authenticate against the server using Http Digest Authentication. +error.closing.output.stream=Error when closing the output stream. +error.committing.output.stream=Error while committing the request output stream. +error.digest.filter.generator=Error during initialization of random generator of Digest authentication. +error.http.method.entity.not.null=Entity must be null for http method {0}. +error.http.method.entity.null=Entity must not be null for http method {0}. +error.service.locator.provider.instance.request=Incorrect type of request instance {0}. Parameter must be a default Jersey ClientRequestContext implementation. +error.service.locator.provider.instance.response=Incorrect type of response instance {0}. Parameter must be a default Jersey ClientResponseContext implementation. +ignored.async.threadpool.size=Zero or negative asynchronous thread pool size specified in the client configuration property: [{0}] \ + Using default cached thread pool. +negative.chunk.size=Negative chunked HTTP transfer coding chunk size value specified in the client configuration property: [{0}] \ + Reverting to programmatically set default: [{1}] +negative.input.parameter="Input parameter {0} must not be negative." +null.connector.provider=ConnectorProvider must not be set to null. +null.executor.service=ExecutorService must not be set to null. +null.input.parameter=Input method parameter {0} must not be null. +null.scheduled.executor.service=ScheduledExecutorService must not be set to null. +null.ssl.context=Custom client SSL context, if set, must not be null. +null.keystore=Custom key store, if set, must not be null. +null.keystore.pasword=Custom key store password must not be null. +null.truststore=Custom trust store, if set, must not be null. +httpurlconnection.replaces.get.with.entity=Detected non-empty entity on a HTTP GET request. The underlying HTTP \ + transport connector may decide to change the request method to POST. +request.entity.writer.null=The entity of the client request is null. +response.to.exception.conversion.failed=Failed to convert a response into an exception. +response.type.is.null=Requested response type is null. +restricted.header.possibly.ignored=Attempt to send restricted header(s) while the [{0}] system property not set. Header(s) will \ + possibly be ignored. +restricted.header.property.setting.false=Restricted headers are not enabled using [{0}] system property (setting only takes \ + effect on connections created after the property has been set/changed). +restricted.header.property.setting.true=Restricted headers are enabled using [{0}] system property(setting only takes effect on\ + connections created after the property has been set/changed). +request.entity.already.written=The entity was already written in this request. The entity can be written (serialized into the output stream) only once per a request. +unexpected.error.response.processing=Unexpected error during response processing. +use.encoding.ignored=Value {1} of {0} client property will be ignored as it is not a valid supported encoding. \ + Valid supported encodings are: {2} +using.fixed.async.threadpool=Using fixed-size thread pool of size [{0}] for asynchronous client invocations. +error.request.cancelled=Request cancelled by the client call. +error.listener.init=ClientLifecycleListener {0} failed to initialize properly. +error.listener.close=ClientLifecycleListener {0} failed to close properly. +error.shutdownhook.close=Client shutdown hook {0} failed.
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/AutoDiscoverableClientTest.java b/core-client/src/test/java/org/glassfish/jersey/client/AutoDiscoverableClientTest.java new file mode 100644 index 0000000..d81380a --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/AutoDiscoverableClientTest.java
@@ -0,0 +1,208 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; + +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.Response; + +import javax.annotation.Priority; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.spi.AutoDiscoverable; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Note: Auto-discoverables from this test "affects" all other tests in suit. + * + * @author Michal Gajdos + */ +public class AutoDiscoverableClientTest { + + private static final String PROPERTY = "AutoDiscoverableTest"; + + private static final ClientRequestFilter component = new ClientRequestFilter() { + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.status(400).entity("CommonAutoDiscoverable").build()); + } + }; + + public static class CommonAutoDiscoverable implements AutoDiscoverable { + + @Override + public void configure(final FeatureContext context) { + // Return if PROPERTY is not true - applicable for other tests. + if (!PropertiesHelper.isProperty(context.getConfiguration().getProperty(PROPERTY))) { + return; + } + + context.register(component, 1); + } + } + + @Priority(10) + public static class AbortFilter implements ClientRequestFilter { + + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.status(400).entity("AbortFilter").build()); + } + } + + public static class FooLifecycleListener implements ContainerRequestFilter, ClientLifecycleListener { + private static boolean CLOSED = false; + private static boolean INITIALIZED = false; + + @Override + public void onInit() { + INITIALIZED = true; + } + + @Override + public void onClose() { + CLOSED = true; + } + + @Override + public void filter(final ContainerRequestContext requestContext) throws IOException { + // do nothing + } + + public static boolean isClosed() { + return CLOSED; + } + + public static boolean isInitialized() { + return INITIALIZED; + } + } + + @ConstrainedTo(RuntimeType.CLIENT) + public static class LifecycleListenerAutoDiscoverable implements AutoDiscoverable { + @Override + public void configure(final FeatureContext context) { + // Return if PROPERTY is not true - applicable for other tests. + if (!PropertiesHelper.isProperty(context.getConfiguration().getProperty(PROPERTY))) { + return; + } + context.register(new FooLifecycleListener(), 1); + } + } + + @Test + public void testAutoDiscoverableGlobalDefaultServerDefault() throws Exception { + _test("CommonAutoDiscoverable", null, null); + } + + @Test + public void testAutoDiscoverableGlobalDefaultServerEnabled() throws Exception { + _test("CommonAutoDiscoverable", null, false); + } + + @Test + public void testAutoDiscoverableGlobalDefaultServerDisabled() throws Exception { + _test("AbortFilter", null, true); + } + + @Test + public void testAutoDiscoverableGlobalDisabledServerDefault() throws Exception { + _test("AbortFilter", true, null); + } + + @Test + public void testAutoDiscoverableGlobalDisabledServerEnabled() throws Exception { + _test("CommonAutoDiscoverable", true, false); + } + + @Test + public void testAutoDiscoverableGlobalDisabledServerDisabled() throws Exception { + _test("AbortFilter", true, true); + } + + @Test + public void testAutoDiscoverableGlobalEnabledServerDefault() throws Exception { + _test("CommonAutoDiscoverable", false, null); + } + + @Test + public void testAutoDiscoverableGlobalEnabledServerEnabled() throws Exception { + _test("CommonAutoDiscoverable", false, false); + } + + @Test + public void testAutoDiscoverableGlobalEnabledServerDisabled() throws Exception { + _test("AbortFilter", false, true); + } + + /** + * Tests, that {@link org.glassfish.jersey.client.ClientLifecycleListener} registered via + * {@link org.glassfish.jersey.internal.spi.AutoDiscoverable} + * {@link javax.ws.rs.core.Feature} will be notified when {@link javax.ws.rs.client.Client#close()} is invoked. + */ + @Test + @Ignore("intermittent failures.") + public void testAutoDiscoverableClosing() { + final ClientConfig config = new ClientConfig(); + config.property(PROPERTY, true); + final JerseyClient client = (JerseyClient) ClientBuilder.newClient(config); + + assertFalse(FooLifecycleListener.isClosed()); + + client.getConfiguration().getRuntime(); // force runtime init + assertTrue("FooLifecycleListener was expected to be already initialized.", FooLifecycleListener.isInitialized()); + assertFalse("FooLifecycleListener was not expected to be closed yet.", FooLifecycleListener.isClosed()); + + client.close(); + + assertTrue("FooLifecycleListener should have been closed.", FooLifecycleListener.isClosed()); + } + + private void _test(final String response, final Boolean globalDisable, final Boolean clientDisable) throws Exception { + final ClientConfig config = new ClientConfig(); + config.register(AbortFilter.class); + config.property(PROPERTY, true); + + if (globalDisable != null) { + config.property(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE, globalDisable); + } + if (clientDisable != null) { + config.property(ClientProperties.FEATURE_AUTO_DISCOVERY_DISABLE, clientDisable); + } + + final Client client = ClientBuilder.newClient(config); + final Invocation.Builder request = client.target("").request(); + + assertEquals(response, request.get().readEntity(String.class)); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/ClientConfigTest.java b/core-client/src/test/java/org/glassfish/jersey/client/ClientConfigTest.java new file mode 100644 index 0000000..558cf69 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/ClientConfigTest.java
@@ -0,0 +1,228 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.Arrays; +import java.util.Map; + +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.internal.util.collection.UnsafeValue; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * {@link ClientConfig} unit test. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ClientConfigTest { + + public ClientConfigTest() { + } + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testSnapshot() { + ClientConfig c_a = new ClientConfig().property("common_name", "common_value"); + + ClientConfig c_b = c_a.snapshot(); + assertNotNull(c_b); + assertNotSame(c_a, c_b); + assertEquals(c_a, c_b); + assertEquals("common_value", c_a.getProperty("common_name")); + assertEquals("common_value", c_b.getProperty("common_name")); + + c_b.property("name", "value"); + + assertFalse(c_a.equals(c_b)); + assertEquals("value", c_b.getProperty("name")); + assertNull(c_a.getProperty("name")); + } + + @Test + public void testGetProperties() { + ClientConfig instance = new ClientConfig(); + Map<String, Object> result = instance.getProperties(); + assertNotNull(result); + + instance.property("name", "value"); + assertEquals("value", result.get("name")); + + try { + result.remove("name"); + fail("UnsupportedOperationException expected"); + } catch (UnsupportedOperationException ex) { + // ok + } + } + + @Test + public void testGetProperty() { + ClientConfig instance = new ClientConfig().property("name", "value"); + assertEquals("value", instance.getProperty("name")); + assertNull(instance.getProperty("other")); + } + + @Provider + public class MyProvider implements ContextResolver<String> { + + @Override + public String getContext(final Class<?> type) { + return ""; + } + } + + @Test + public void testCustomProvidersFeature() { + final CustomProvidersFeature feature = new CustomProvidersFeature(Arrays.asList(new Class<?>[]{MyProvider.class})); + + ClientConfig instance = new ClientConfig(); + instance.register(feature); + + assertTrue(instance.getClasses().isEmpty()); + assertEquals(1, instance.getInstances().size()); + + // Features are registered at the time of provider bindings. + final JerseyClient jerseyClient = new JerseyClient(instance, (UnsafeValue<SSLContext, IllegalStateException>) null, null); + ClientConfig config = jerseyClient.getConfiguration(); + final ClientRuntime runtime = config.getRuntime(); + + final ClientConfig runtimeConfig = runtime.getConfig(); + + assertTrue(runtimeConfig.isRegistered(MyProvider.class)); + assertTrue(runtimeConfig.isEnabled(feature)); + + } + + public static class EmptyFeature implements Feature { + + @Override + public boolean configure(final FeatureContext context) { + return true; + } + } + + public static class UnconfigurableFeature implements Feature { + + @Override + public boolean configure(final FeatureContext context) { + return false; + } + } + + /** + * Copied from DefaultConfigTest. + */ + @Test + public void testGetFeatures() { + final EmptyFeature emptyFeature = new EmptyFeature(); + final UnconfigurableFeature unconfigurableFeature = new UnconfigurableFeature(); + + ClientConfig clientConfig = new ClientConfig(); + clientConfig.register(emptyFeature); + clientConfig.register(unconfigurableFeature); + + // Features are registered at the time of provider bindings. + final JerseyClient jerseyClient = + new JerseyClient(clientConfig, (UnsafeValue<SSLContext, IllegalStateException>) null, null); + clientConfig = jerseyClient.getConfiguration(); + clientConfig.getRuntime(); + + final Configuration runtimeConfig = clientConfig.getRuntime().getConfig(); + + assertTrue(runtimeConfig.isEnabled(emptyFeature)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGetProviderClasses() { + ClientConfig instance = new ClientConfig(); + instance.getClasses().add(ClientConfigTest.class); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGetProviderInstances() { + ClientConfig instance = new ClientConfig(); + instance.getInstances().add(this); + } + + @Test + public void testUpdate() { + final UnconfigurableFeature unconfigurableFeature = new UnconfigurableFeature(); + + ClientConfig clientConfig1 = new ClientConfig(); + ClientConfig clientConfig2 = new ClientConfig(); + + clientConfig1.register(EmptyFeature.class); + clientConfig2.register(unconfigurableFeature); + + ClientConfig clientConfig = clientConfig2.loadFrom(clientConfig1); + + // Features are registered at the time of provider bindings. + final JerseyClient jerseyClient = + new JerseyClient(clientConfig, (UnsafeValue<SSLContext, IllegalStateException>) null, null); + clientConfig = jerseyClient.getConfiguration(); + clientConfig.getRuntime(); + + final Configuration runtimeConfig = clientConfig.getRuntime().getConfig(); + + assertTrue(runtimeConfig.isEnabled(EmptyFeature.class)); + } + + @Test + public void testSetProperty() { + ClientConfig instance = new ClientConfig(); + assertTrue(instance.getProperties().isEmpty()); + + instance.property("name", "value"); + assertFalse(instance.getProperties().isEmpty()); + assertEquals(1, instance.getProperties().size()); + assertEquals("value", instance.getProperty("name")); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/ClientRequestTest.java b/core-client/src/test/java/org/glassfish/jersey/client/ClientRequestTest.java new file mode 100644 index 0000000..77512ca --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/ClientRequestTest.java
@@ -0,0 +1,196 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.net.URI; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.WriterInterceptor; + +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.internal.util.ExceptionUtils; +import org.glassfish.jersey.message.MessageBodyWorkers; + +import org.hamcrest.core.Is; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.runners.MockitoJUnitRunner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.same; + +/** + * {@code ClientRequest} unit tests. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@RunWith(MockitoJUnitRunner.class) +public class ClientRequestTest { + + @Mock + private MessageBodyWorkers workers; + @Mock + private GenericType<?> entityType; + + @Before + public void initMocks() { + MockitoAnnotations.initMocks(this); + } + + /** + * Test of resolving properties in the client request. + */ + @Test + public void testResolveProperty() { + JerseyClient client; + ClientRequest request; + + // test property in neither config nor request + client = new JerseyClientBuilder().build(); + + request = new ClientRequest( + URI.create("http://example.org"), + client.getConfiguration(), + new MapPropertiesDelegate()); + + assertFalse(request.getConfiguration().getPropertyNames().contains("name")); + assertFalse(request.getPropertyNames().contains("name")); + + assertNull(request.getConfiguration().getProperty("name")); + assertNull(request.getProperty("name")); + + assertNull(request.resolveProperty("name", String.class)); + assertEquals("value-default", request.resolveProperty("name", "value-default")); + + // test property in config only + client = new JerseyClientBuilder().property("name", "value-global").build(); + + request = new ClientRequest( + URI.create("http://example.org"), + client.getConfiguration(), + new MapPropertiesDelegate()); + + assertTrue(request.getConfiguration().getPropertyNames().contains("name")); + assertFalse(request.getPropertyNames().contains("name")); + + assertEquals("value-global", request.getConfiguration().getProperty("name")); + assertNull(request.getProperty("name")); + + assertEquals("value-global", request.resolveProperty("name", String.class)); + assertEquals("value-global", request.resolveProperty("name", "value-default")); + + // test property in request only + client = new JerseyClientBuilder().build(); + + request = new ClientRequest( + URI.create("http://example.org"), + client.getConfiguration(), + new MapPropertiesDelegate()); + request.setProperty("name", "value-request"); + + assertFalse(request.getConfiguration().getPropertyNames().contains("name")); + assertTrue(request.getPropertyNames().contains("name")); + + assertNull(request.getConfiguration().getProperty("name")); + assertEquals("value-request", request.getProperty("name")); + + assertEquals("value-request", request.resolveProperty("name", String.class)); + assertEquals("value-request", request.resolveProperty("name", "value-default")); + + // test property in config and request + client = new JerseyClientBuilder().property("name", "value-global").build(); + + request = new ClientRequest( + URI.create("http://example.org"), + client.getConfiguration(), + new MapPropertiesDelegate()); + request.setProperty("name", "value-request"); + + assertTrue(request.getConfiguration().getPropertyNames().contains("name")); + assertTrue(request.getPropertyNames().contains("name")); + + assertEquals("value-global", request.getConfiguration().getProperty("name")); + assertEquals("value-request", request.getProperty("name")); + + assertEquals("value-request", request.resolveProperty("name", String.class)); + assertEquals("value-request", request.resolveProperty("name", "value-default")); + } + + private ClientRequest mockThrowing(Exception exception) throws IOException { + JerseyClient client = new JerseyClientBuilder().build(); + final ClientRequest request = new ClientRequest( + URI.create("http://example.org"), + client.getConfiguration(), + new MapPropertiesDelegate()); + + Mockito.doThrow(exception).when(workers) + .writeTo(any(), same(entityType.getRawType()), same(entityType.getType()), + Mockito.<Annotation[]>any(), Mockito.<MediaType>any(), + Mockito.<MultivaluedMap<String, Object>>any(), Mockito.<PropertiesDelegate>any(), + Mockito.<OutputStream>any(), Mockito.<Iterable<WriterInterceptor>>any()); + return request; + } + + @Test + public void testSSLExceptionHandling() + throws Exception { + final IOException ioException = new IOException("Test"); + + final ClientRequest request = mockThrowing(ioException); + + try { + request.doWriteEntity(workers, entityType); + fail("An IOException exception should be thrown."); + } catch (IOException e) { + Assert.assertThat("Detected a un-expected exception! \n" + ExceptionUtils.exceptionStackTraceAsString(e), + e, Is.is(ioException)); + } + } + + @Test + public void testRuntimeExceptionBeingReThrown() + throws Exception { + + final RuntimeException runtimeException = new RuntimeException("Test"); + + ClientRequest request = mockThrowing(runtimeException); + + try { + request.doWriteEntity(workers, entityType); + Assert.fail("A RuntimeException exception should be thrown."); + } catch (RuntimeException e) { + Assert.assertThat("Detected a un-expected exception! \n" + ExceptionUtils.exceptionStackTraceAsString(e), + e, Is.is(runtimeException)); + } + } + +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/ClientRxTest.java b/core-client/src/test/java/org/glassfish/jersey/client/ClientRxTest.java new file mode 100644 index 0000000..263822d --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/ClientRxTest.java
@@ -0,0 +1,149 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.RxInvokerProvider; +import javax.ws.rs.client.SyncInvoker; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.ext.Provider; + +import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder; + +import org.hamcrest.core.AllOf; +import org.hamcrest.core.StringContains; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.junit.Assert.assertTrue; + +/** + * Sanity test for {@link Invocation.Builder#rx()} methods. + * + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class ClientRxTest { + + private static final ExecutorService EXECUTOR_SERVICE = + Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("rxTest-%d").build()); + + private final Client CLIENT; + private final Client CLIENT_WITH_EXECUTOR; + + public ClientRxTest() { + CLIENT = ClientBuilder.newClient(); + + // TODO JAX-RS 2.1 + // CLIENT_WITH_EXECUTOR = ClientBuilder.newBuilder().executorService(EXECUTOR_SERVICE).build(); + CLIENT_WITH_EXECUTOR = null; + } + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @After + public void afterClass() { + CLIENT.close(); + } + + @Test + public void testRxInvoker() { + // explicit register is not necessary, but it can be used. + CLIENT.register(TestRxInvokerProvider.class, RxInvokerProvider.class); + + String s = target(CLIENT).request().rx(TestRxInvoker.class).get(); + + assertTrue("Provided RxInvoker was not used.", s.startsWith("rxTestInvoker")); + } + + @Test + @Ignore("TODO JAX-RS 2.1") + public void testRxInvokerWithExecutor() { + // implicit register (not saying that the contract is RxInvokerProvider). + CLIENT.register(TestRxInvokerProvider.class); + + ExecutorService executorService = Executors + .newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("rxTest-%d").build()); + String s = target(CLIENT_WITH_EXECUTOR).request().rx(TestRxInvoker.class).get(); + + assertTrue("Provided RxInvoker was not used.", s.startsWith("rxTestInvoker")); + assertTrue("Executor Service was not passed to RxInvoker", s.contains("rxTest-")); + } + + @Test + public void testRxInvokerInvalid() { + Invocation.Builder request = target(CLIENT).request(); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage(AllOf.allOf(new StringContains("null"), new StringContains("clazz"))); + request.rx(null).get(); + } + + @Test + public void testRxInvokerNotRegistered() { + Invocation.Builder request = target(CLIENT).request(); + thrown.expect(IllegalStateException.class); + thrown.expectMessage(AllOf.allOf( + new StringContains("TestRxInvoker"), + new StringContains("not registered"), + new StringContains("RxInvokerProvider"))); + request.rx(TestRxInvoker.class).get(); + } + + private WebTarget target(Client client) { + // Uri is not relevant, the call won't be ever executed. + return client.target("http://localhost:9999"); + } + + @Provider + public static class TestRxInvokerProvider implements RxInvokerProvider<TestRxInvoker> { + @Override + public TestRxInvoker getRxInvoker(SyncInvoker syncInvoker, ExecutorService executorService) { + return new TestRxInvoker(syncInvoker, executorService); + } + + @Override + public boolean isProviderFor(Class<?> clazz) { + return TestRxInvoker.class.equals(clazz); + } + } + + private static class TestRxInvoker extends AbstractRxInvoker<String> { + + private TestRxInvoker(SyncInvoker syncInvoker, ExecutorService executor) { + super(syncInvoker, executor); + } + + @Override + public <R> String method(String name, Entity<?> entity, Class<R> responseType) { + return "rxTestInvoker" + (getExecutorService() == null ? "" : " rxTest-"); + } + + @Override + public <R> String method(String name, Entity<?> entity, GenericType<R> responseType) { + return "rxTestInvoker" + (getExecutorService() == null ? "" : " rxTest-"); + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/CustomConnectorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/CustomConnectorTest.java new file mode 100644 index 0000000..570e60c --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/CustomConnectorTest.java
@@ -0,0 +1,223 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.util.concurrent.Future; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class CustomConnectorTest { + + public static class NullConnector implements Connector, ConnectorProvider { + + @Override + public ClientResponse apply(ClientRequest request) { + throw new ProcessingException("test"); + } + + @Override + public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) { + throw new ProcessingException("test-async"); + } + + @Override + public void close() { + // do nothing + } + + @Override + public String getName() { + return null; + } + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return this; + } + } + + @Test + public void testNullConnector() { + Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new NullConnector()).getConfiguration()); + try { + client.target(UriBuilder.fromUri("/").build()).request().get(); + } catch (ProcessingException ce) { + assertEquals("test", ce.getMessage()); + } + try { + client.target(UriBuilder.fromUri("/").build()).request().async().get(); + } catch (ProcessingException ce) { + assertEquals("test-async", ce.getMessage()); + } + } + + /** + * Loop-back connector provider. + */ + public static class TestConnectorProvider implements ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return new TestConnector(); + } + + } + + /** + * Loop-back connector. + */ + public static class TestConnector implements Connector { + /** + * Test loop-back status code. + */ + public static final int TEST_LOOPBACK_CODE = 600; + /** + * Test loop-back status type. + */ + public final Response.StatusType LOOPBACK_STATUS = new Response.StatusType() { + @Override + public int getStatusCode() { + return TEST_LOOPBACK_CODE; + } + + @Override + public Response.Status.Family getFamily() { + return Response.Status.Family.OTHER; + } + + @Override + public String getReasonPhrase() { + return "Test connector loop-back"; + } + }; + + private volatile boolean closed = false; + + @Override + public ClientResponse apply(ClientRequest request) { + checkNotClosed(); + final ClientResponse response = new ClientResponse(LOOPBACK_STATUS, request); + + response.setEntityStream(new ByteArrayInputStream(request.getUri().toString().getBytes())); + return response; + } + + @Override + public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) { + checkNotClosed(); + throw new UnsupportedOperationException("Async invocation not supported by the test connector."); + } + + @Override + public String getName() { + return "test-loop-back-connector"; + } + + @Override + public void close() { + closed = true; + } + + private void checkNotClosed() { + if (closed) { + throw new IllegalStateException("Connector closed."); + } + } + } + + /** + * Test client request filter that creates new client based on the current runtime configuration + * and uses the new client to produce a response. + */ + public static class TestClientFilter implements ClientRequestFilter { + + private static final String INVOKED_BY_TEST_FILTER = "invoked-by-test-filter"; + + @Override + public void filter(ClientRequestContext requestContext) { + final Configuration config = requestContext.getConfiguration(); + final JerseyClient client = new JerseyClientBuilder().withConfig(config).build(); + + try { + if (requestContext.getPropertyNames().contains(INVOKED_BY_TEST_FILTER)) { + return; // prevent the infinite recursion... + } + + final URI filteredUri = UriBuilder.fromUri(requestContext.getUri()).path("filtered").build(); + requestContext.abortWith(client.target(filteredUri).request().property(INVOKED_BY_TEST_FILTER, true).get()); + } finally { + client.close(); + } + } + } + + /** + * Reproducer for JERSEY-2318. + * + * The test verifies that the {@link org.glassfish.jersey.client.spi.ConnectorProvider} configured + * on one client instance is transferred to another client instance when the new client instance is + * created from the original client instance configuration. + */ + @Test + public void testConnectorProviderPreservedOnClientConfigCopy() { + final ClientConfig clientConfig = new ClientConfig().connectorProvider(new TestConnectorProvider()); + + final Client client = ClientBuilder.newClient(clientConfig); + try { + Response response; + + final WebTarget target = client.target("http://wherever.org/"); + response = target.request().get(); + // let's first verify we are using the test loop-back connector. + assertThat(response.getStatus(), equalTo(TestConnector.TEST_LOOPBACK_CODE)); + assertThat(response.readEntity(String.class), equalTo("http://wherever.org/")); + + // and now with the filter... + target.register(TestClientFilter.class); + response = target.request().get(); + // check if the connector provider has been propagated: + assertThat(response.getStatus(), equalTo(TestConnector.TEST_LOOPBACK_CODE)); + // check if the filter has been invoked: + assertThat(response.readEntity(String.class), equalTo("http://wherever.org/filtered")); + } finally { + client.close(); + } + } + +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/DefaultSslContextProviderTest.java b/core-client/src/test/java/org/glassfish/jersey/client/DefaultSslContextProviderTest.java new file mode 100644 index 0000000..b7483d9 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/DefaultSslContextProviderTest.java
@@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.spi.DefaultSslContextProvider; +import org.glassfish.jersey.internal.util.collection.UnsafeValue; +import org.glassfish.jersey.internal.util.collection.Values; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class DefaultSslContextProviderTest { + + @Test + public void testProvidedDefaultSslContextProvider() { + + final ClientConfig clientConfig = new ClientConfig(); + + final AtomicBoolean getDefaultSslContextCalled = new AtomicBoolean(false); + + + final JerseyClient jerseyClient = + new JerseyClient(clientConfig, SslConfigurator.getDefaultContext(), + null, new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + getDefaultSslContextCalled.set(true); + + return SslConfigurator.getDefaultContext(); + } + }); + + jerseyClient.getSslContext(); + + assertFalse(getDefaultSslContextCalled.get()); + assertFalse(jerseyClient.isDefaultSslContext()); + } + + @Test + public void testProvidedDefaultSslContextProviderUnsafeVal() { + + final ClientConfig clientConfig = new ClientConfig(); + + final AtomicBoolean getDefaultSslContextCalled = new AtomicBoolean(false); + + + final JerseyClient jerseyClient = + new JerseyClient(clientConfig, Values.<SSLContext, + IllegalStateException>unsafe(SslConfigurator.getDefaultContext()), + null, new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + getDefaultSslContextCalled.set(true); + + return SslConfigurator.getDefaultContext(); + } + }); + + jerseyClient.getSslContext(); + + assertFalse(getDefaultSslContextCalled.get()); + assertFalse(jerseyClient.isDefaultSslContext()); + } + + @Test + public void testCustomDefaultSslContextProvider() { + + final ClientConfig clientConfig = new ClientConfig(); + + final AtomicBoolean getDefaultSslContextCalled = new AtomicBoolean(false); + final AtomicReference<SSLContext> returnedContext = new AtomicReference<SSLContext>(null); + + final JerseyClient jerseyClient = + new JerseyClient(clientConfig, (SSLContext) null, + null, new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + getDefaultSslContextCalled.set(true); + + final SSLContext defaultSslContext = SslConfigurator.getDefaultContext(); + returnedContext.set(defaultSslContext); + return defaultSslContext; + } + }); + + // make sure context is created + jerseyClient.getSslContext(); + + assertEquals(returnedContext.get(), jerseyClient.getSslContext()); + assertTrue(getDefaultSslContextCalled.get()); + assertTrue(jerseyClient.isDefaultSslContext()); + } + + @Test + public void testCustomDefaultSslContextProviderUnsafeVal() { + + final ClientConfig clientConfig = new ClientConfig(); + + final AtomicBoolean getDefaultSslContextCalled = new AtomicBoolean(false); + final AtomicReference<SSLContext> returnedContext = new AtomicReference<SSLContext>(null); + + final JerseyClient jerseyClient = + new JerseyClient(clientConfig, (UnsafeValue<SSLContext, IllegalStateException>) null, + null, new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + getDefaultSslContextCalled.set(true); + + final SSLContext defaultSslContext = SslConfigurator.getDefaultContext(); + returnedContext.set(defaultSslContext); + return defaultSslContext; + } + }); + + // make sure context is created + jerseyClient.getSslContext(); + + assertEquals(returnedContext.get(), jerseyClient.getSslContext()); + assertTrue(getDefaultSslContextCalled.get()); + assertTrue(jerseyClient.isDefaultSslContext()); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/FixedBoundaryParserTest.java b/core-client/src/test/java/org/glassfish/jersey/client/FixedBoundaryParserTest.java new file mode 100644 index 0000000..16b956f --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/FixedBoundaryParserTest.java
@@ -0,0 +1,152 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Tests several parsing use-cases of ChunkedInput + * + * @author Petr Bouda + **/ +public class FixedBoundaryParserTest { + + public static final String DELIMITER_4 = "1234"; + + public static final String DELIMITER_1 = "#"; + + @Test + public void testFixedBoundaryParserNullInput() throws IOException { + final ChunkParser parser = ChunkedInput.createParser(DELIMITER_4); + InputStream input = new ByteArrayInputStream(new byte[] {}); + assertNull(parser.readChunk(input)); + } + + @Test + public void testFixedBoundaryParserDelimiter4() throws IOException { + final ChunkParser parser = ChunkedInput.createParser(DELIMITER_4); + + // delimiter is the same char sequence as an input + assertNull(parse(parser, DELIMITER_4)); + + // input starts with the delimiter + assertEquals("123", parse(parser, DELIMITER_4 + "123")); + + // beginning of the input and delimiter are not the same + assertEquals("abc", parse(parser, "abc" + DELIMITER_4 + "def")); + + // delimiter in the input is not complete, only partial + assertEquals("abc123", parse(parser, "abc123")); + + // delimiter in the input is not complete, only partial, + // and then continue with a char which is not part of the + // delimiter + assertEquals("abc1235", parse(parser, "abc1235")); + + // delimiter in the input is not complete, only partial, + // and then continue with a char which is part of the + // delimiter + assertEquals("abc1231", parse(parser, "abc1231")); + + // input has the same beginning as the delimiter + assertEquals("12", parse(parser, "121234")); + + // input ends with first char of delimiter + assertEquals("1231", parse(parser, "1231")); + } + + @Test + public void testFixedBoundaryParserDelimiter1() throws IOException { + final ChunkParser parser = ChunkedInput.createParser(DELIMITER_1); + + // delimiter is the same char sequence as an input + assertNull(parse(parser, DELIMITER_1)); + + // input starts with the delimiter + assertEquals("123", parse(parser, DELIMITER_1 + "123")); + + // beginning of the input and delimiter are not the same + assertEquals("abc", parse(parser, "abc" + DELIMITER_1 + "def")); + + // delimiter in the input is not complete, only partial + assertEquals("abc123", parse(parser, "abc123")); + } + + @Test + public void delimiterWithRepeatedInitialCharacters() throws IOException { + ChunkParser parser = ChunkedInput.createParser("**b**"); + assertEquals("1*", parse(parser, "1***b**")); + } + + private static String parse(ChunkParser parser, String str) throws IOException { + InputStream input = new ByteArrayInputStream(str.getBytes()); + byte[] bytes = parser.readChunk(input); + return bytes == null ? null : new String(bytes); + } + + @Test + public void testFixedBoundaryParserFlow() throws IOException { + final ChunkParser parser = ChunkedInput.createParser(DELIMITER_4); + + String input = "abc" + DELIMITER_4 + "edf" + DELIMITER_4 + "ghi"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("ghi", new String(bytes)); + } + + @Test + public void testFixedBoundaryParserFlowDelimiterFirst() throws IOException { + final ChunkParser parser = ChunkedInput.createParser(DELIMITER_4); + + String input = DELIMITER_4 + "edf" + DELIMITER_4 + "ghi"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("ghi", new String(bytes)); + } + + @Test + public void testFixedBoundaryParserFlowDelimiterEnds() throws IOException { + final ChunkParser parser = ChunkedInput.createParser(DELIMITER_4); + + String input = "abc" + DELIMITER_4 + "edf" + DELIMITER_4; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + } + +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/FixedMultiBoundaryParserTest.java b/core-client/src/test/java/org/glassfish/jersey/client/FixedMultiBoundaryParserTest.java new file mode 100644 index 0000000..936d6a4 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/FixedMultiBoundaryParserTest.java
@@ -0,0 +1,205 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Tests several parsing use-cases of ChunkedInput + * + * @author Petr Bouda + **/ +public class FixedMultiBoundaryParserTest { + + public static final String DELIMITER_4 = "1234"; + + public static final String DELIMITER_1 = "#"; + + @Test + public void testFixedBoundaryParserNullInput() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser(DELIMITER_4); + InputStream input = new ByteArrayInputStream(new byte[] {}); + assertNull(parser.readChunk(input)); + } + + @Test + public void testFixedBoundaryParserDelimiter4() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser(DELIMITER_4); + + // delimiter is the same char sequence as an input + assertNull(parse(parser, DELIMITER_4)); + + // input starts with the delimiter + assertEquals("123", parse(parser, DELIMITER_4 + "123")); + + // beginning of the input and delimiter are not the same + assertEquals("abc", parse(parser, "abc" + DELIMITER_4 + "def")); + + // delimiter in the input is not complete, only partial + assertEquals("abc123", parse(parser, "abc123")); + + // delimiter in the input is not complete, only partial, + // and then continue with a char which is not part of the + // delimiter + assertEquals("abc1235", parse(parser, "abc1235")); + + // delimiter in the input is not complete, only partial, + // and then continue with a char which is part of the + // delimiter + assertEquals("abc1231", parse(parser, "abc1231")); + + // input has the same beginning as the delimiter + assertEquals("12", parse(parser, "121234")); + + // input ends with first char of delimiter + assertEquals("1231", parse(parser, "1231")); + } + + @Test + public void testFixedBoundaryParserDelimiter1() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser(DELIMITER_1); + + // delimiter is the same char sequence as an input + assertNull(parse(parser, DELIMITER_1)); + + // input starts with the delimiter + assertEquals("123", parse(parser, DELIMITER_1 + "123")); + + // beginning of the input and delimiter are not the same + assertEquals("abc", parse(parser, "abc" + DELIMITER_1 + "def")); + + // delimiter in the input is not complete, only partial + assertEquals("abc123", parse(parser, "abc123")); + } + + @Test + public void delimiterWithRepeatedInitialCharacters() throws IOException { + ChunkParser parser = ChunkedInput.createMultiParser("**b**"); + assertEquals("1*", parse(parser, "1***b**")); + } + + private static String parse(ChunkParser parser, String str) throws IOException { + InputStream input = new ByteArrayInputStream(str.getBytes()); + byte[] bytes = parser.readChunk(input); + return bytes == null ? null : new String(bytes); + } + + @Test + public void testFixedBoundaryParserFlow() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser(DELIMITER_4); + + String input = "abc" + DELIMITER_4 + "edf" + DELIMITER_4 + "ghi"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("ghi", new String(bytes)); + } + + @Test + public void testFixedBoundaryParserFlowDelimiterFirst() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser(DELIMITER_4); + + String input = DELIMITER_4 + "edf" + DELIMITER_4 + "ghi"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("ghi", new String(bytes)); + } + + @Test + public void testFixedBoundaryParserFlowDelimiterEnds() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser(DELIMITER_4); + + String input = "abc" + DELIMITER_4 + "edf" + DELIMITER_4; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + } + + @Test + public void testMultiFixedBoundaryParserCRLF() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser("\n\n", "\r\n\r\n"); + + String input = "abc" + "\r\n\r\n" + "edf" + "\r\n\r\n"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + } + + @Test + public void testMultiFixedBoundaryParserCRLFStart() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser("\n\n", "\r\n\r\n"); + + String input = "\r\n\r\n" + "edf" + "\r\n\r\n"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + } + + @Test + public void testMultiFixedBoundaryParserLF() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser("\n\n", "\r\n\r\n"); + + String input = "\n\n" + "abc" + "\n\n"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + } + + @Test + public void testMultiFixedBoundaryParserCRLFwithLF() throws IOException { + final ChunkParser parser = ChunkedInput.createMultiParser("\n\n", "\r\n\r\n"); + + String input = "abc" + "\r\n\r\n" + "edf" + "\n\n" + "ghi"; + InputStream stream = new ByteArrayInputStream(input.getBytes()); + + byte[] bytes = parser.readChunk(stream); + assertEquals("abc", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("edf", new String(bytes)); + + bytes = parser.readChunk(stream); + assertEquals("ghi", new String(bytes)); + } + +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorTest.java new file mode 100644 index 0000000..7cd9121 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorTest.java
@@ -0,0 +1,783 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URL; +import java.security.Permission; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.Response; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +import org.glassfish.jersey.client.internal.HttpUrlConnector; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Various tests for the default client connector. + * + * @author Carlo Pellegrini + * @author Miroslav Fuksa + * @author Marek Potociar + * @author Jakub Podlesak + */ +public class HttpUrlConnectorTest { + + // there is about 30 ms overhead on my laptop, 500 ms should be safe + private final int TimeoutBASE = 500; + + /** + * Reproducer for JERSEY-1984. + * TODO: fix and re-enable the test, it could give java.net.NoRouteToHostException in certain environments instead of timeout exception + */ + @Test + @Ignore + public void testConnectionTimeoutWithEntity() { + _testInvocationTimeout(createNonRoutableTarget().request().buildPost(Entity.text("does not matter"))); + } + + /** + * Additional test case for JERSEY-1984 to ensure, that the error occurs only when sending an entity. + * TODO: see above, rewrite server part, the "non-routable" target solution is fragile + */ + @Test + @Ignore + public void testConnectionTimeoutNoEntity() { + _testInvocationTimeout(createNonRoutableTarget().request().buildGet()); + } + + /** + * Reproducer for JERSEY-1611. + */ + @Test + public void testResolvedRequestUri() { + HttpUrlConnectorProvider.ConnectionFactory factory = new HttpUrlConnectorProvider.ConnectionFactory() { + @Override + public HttpURLConnection getConnection(URL endpointUrl) throws IOException { + HttpURLConnection result = (HttpURLConnection) endpointUrl.openConnection(); + return wrapRedirectedHttp(result); + } + }; + JerseyClient client = new JerseyClientBuilder().build(); + + ClientRequest request = client.target("http://localhost:8080").request().buildGet().request(); + final HttpUrlConnectorProvider connectorProvider = new HttpUrlConnectorProvider().connectionFactory(factory); + HttpUrlConnector connector = (HttpUrlConnector) connectorProvider.getConnector(client, client.getConfiguration()); + ClientResponse res = connector.apply(request); + assertEquals(URI.create("http://localhost:8080"), res.getRequestContext().getUri()); + assertEquals(URI.create("http://redirected.org:8080/redirected"), res.getResolvedRequestUri()); + + + res.getHeaders().putSingle(HttpHeaders.LINK, Link.fromPath("action").rel("test").build().toString()); + assertEquals(URI.create("http://redirected.org:8080/action"), res.getLink("test").getUri()); + } + + private HttpURLConnection wrapRedirectedHttp(final HttpURLConnection connection) { + if (connection instanceof HttpsURLConnection) { + return connection; + } + + return new HttpURLConnection(connection.getURL()) { + + @Override + public URL getURL() { + return url; + } + + @Override + public int getResponseCode() throws IOException { + // Pretend we redirected for testRedirection + url = new URL("http://redirected.org:8080/redirected"); + // and fake the status code to prevent actual connection + return Response.Status.NO_CONTENT.getStatusCode(); + } + + @Override + public String getResponseMessage() throws IOException { + return Response.Status.NO_CONTENT.getReasonPhrase(); + } + + // Ignored for compatibility on java6 + // @Override + public long getContentLengthLong() { + return connection.getContentLength(); + // Ignored for compatibility on java6 + // return delegate.getContentLengthLong(); + } + + // Ignored for compatibility on java6 + // @Override + public long getHeaderFieldLong(String name, long Default) { + return connection.getHeaderFieldInt(name, (int) Default); + // Ignored for compatibility on java6 + // return delegate.getHeaderFieldLong(name, Default); + } + + // Ignored for compatibility on java6 + // @Override + public void setFixedLengthStreamingMode(long contentLength) { + // Ignored for compatibility on java6 + // delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + connection.setInstanceFollowRedirects(followRedirects); + } + + @Override + public boolean getInstanceFollowRedirects() { + return connection.getInstanceFollowRedirects(); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + connection.setRequestMethod(method); + } + + @Override + public String getRequestMethod() { + return connection.getRequestMethod(); + } + + @Override + public long getHeaderFieldDate(String name, long Default) { + return connection.getHeaderFieldDate(name, Default); + } + + @Override + public void disconnect() { + connection.disconnect(); + } + + @Override + public boolean usingProxy() { + return connection.usingProxy(); + } + + @Override + public Permission getPermission() throws IOException { + return connection.getPermission(); + } + + @Override + public InputStream getErrorStream() { + return connection.getErrorStream(); + } + + @Override + public String getHeaderField(int n) { + return connection.getHeaderField(n); + } + + @Override + public void setChunkedStreamingMode(int chunklen) { + connection.setChunkedStreamingMode(chunklen); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + connection.setFixedLengthStreamingMode(contentLength); + } + + @Override + public String getHeaderFieldKey(int n) { + return connection.getHeaderFieldKey(n); + } + + @Override + public void connect() throws IOException { + connection.connect(); + } + + @Override + public void setConnectTimeout(int timeout) { + connection.setConnectTimeout(timeout); + } + + @Override + public int getConnectTimeout() { + return connection.getConnectTimeout(); + } + + @Override + public void setReadTimeout(int timeout) { + connection.setReadTimeout(timeout); + } + + @Override + public int getReadTimeout() { + return connection.getReadTimeout(); + } + + @Override + public int getContentLength() { + return connection.getContentLength(); + } + + @Override + public String getContentType() { + return connection.getContentType(); + } + + @Override + public String getContentEncoding() { + return connection.getContentEncoding(); + } + + @Override + public long getExpiration() { + return connection.getExpiration(); + } + + @Override + public long getDate() { + return connection.getDate(); + } + + @Override + public long getLastModified() { + return connection.getLastModified(); + } + + @Override + public String getHeaderField(String name) { + return connection.getHeaderField(name); + } + + @Override + public Map<String, List<String>> getHeaderFields() { + return connection.getHeaderFields(); + } + + @Override + public int getHeaderFieldInt(String name, int Default) { + return connection.getHeaderFieldInt(name, Default); + } + + @Override + public Object getContent() throws IOException { + return connection.getContent(); + } + + @Override + public Object getContent(Class[] classes) throws IOException { + return connection.getContent(classes); + } + + @Override + public InputStream getInputStream() throws IOException { + return connection.getInputStream(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return connection.getOutputStream(); + } + + @Override + public String toString() { + return connection.toString(); + } + + @Override + public void setDoInput(boolean doinput) { + connection.setDoInput(doinput); + } + + @Override + public boolean getDoInput() { + return connection.getDoInput(); + } + + @Override + public void setDoOutput(boolean dooutput) { + connection.setDoOutput(dooutput); + } + + @Override + public boolean getDoOutput() { + return connection.getDoOutput(); + } + + @Override + public void setAllowUserInteraction(boolean allowuserinteraction) { + connection.setAllowUserInteraction(allowuserinteraction); + } + + @Override + public boolean getAllowUserInteraction() { + return connection.getAllowUserInteraction(); + } + + @Override + public void setUseCaches(boolean usecaches) { + connection.setUseCaches(usecaches); + } + + @Override + public boolean getUseCaches() { + return connection.getUseCaches(); + } + + @Override + public void setIfModifiedSince(long ifmodifiedsince) { + connection.setIfModifiedSince(ifmodifiedsince); + } + + @Override + public long getIfModifiedSince() { + return connection.getIfModifiedSince(); + } + + @Override + public boolean getDefaultUseCaches() { + return connection.getDefaultUseCaches(); + } + + @Override + public void setDefaultUseCaches(boolean defaultusecaches) { + connection.setDefaultUseCaches(defaultusecaches); + } + + @Override + public void setRequestProperty(String key, String value) { + connection.setRequestProperty(key, value); + } + + @Override + public void addRequestProperty(String key, String value) { + connection.addRequestProperty(key, value); + } + + @Override + public String getRequestProperty(String key) { + return connection.getRequestProperty(key); + } + + @Override + public Map<String, List<String>> getRequestProperties() { + return connection.getRequestProperties(); + } + }; + + } + + private void _testInvocationTimeout(Invocation invocation) { + + final long start = System.currentTimeMillis(); + + try { + invocation.invoke(); + + Assert.fail("Timeout expected!"); + + } catch (Exception ex) { + + Assert.assertTrue(String.format("Bad exception, %s, caught! Timeout expected.", ex.getCause()), + ex.getCause() instanceof SocketTimeoutException); + + final long stop = System.currentTimeMillis(); + long time = stop - start; + + Assert.assertTrue( + String.format( + "Actual time, %d ms, should not be more than twice as longer as the original timeout, %d ms", + time, TimeoutBASE), + time < 2 * TimeoutBASE); + } + } + + /** + * Test SSL connection. + */ + @Test + public void testSSLConnection() { + JerseyClient client = new JerseyClientBuilder().build(); + ClientRequest request = client.target("https://localhost:8080").request().buildGet().request(); + HttpUrlConnectorProvider.ConnectionFactory factory = new HttpUrlConnectorProvider.ConnectionFactory() { + @Override + public HttpURLConnection getConnection(URL endpointUrl) throws IOException { + HttpURLConnection result = (HttpURLConnection) endpointUrl.openConnection(); + return wrapNoContentHttps(result); + } + }; + final HttpUrlConnectorProvider connectorProvider = new HttpUrlConnectorProvider().connectionFactory(factory); + HttpUrlConnector connector = (HttpUrlConnector) connectorProvider.getConnector(client, client.getConfiguration()); + ClientResponse res = connector.apply(request); + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), res.getStatusInfo().getStatusCode()); + assertEquals(Response.Status.NO_CONTENT.getReasonPhrase(), res.getStatusInfo().getReasonPhrase()); + } + + private HttpURLConnection wrapNoContentHttps(final HttpURLConnection result) { + if (result instanceof HttpsURLConnection) { + return new HttpsURLConnection(result.getURL()) { + private final HttpsURLConnection delegate = (HttpsURLConnection) result; + + @Override + public int getResponseCode() throws IOException { + return Response.Status.NO_CONTENT.getStatusCode(); + } + + @Override + public String getResponseMessage() throws IOException { + return Response.Status.NO_CONTENT.getReasonPhrase(); + } + + // Ignored for compatibility on java6 + // @Override + public long getContentLengthLong() { + return delegate.getContentLength(); + // Ignored for compatibility on java6 + // return delegate.getContentLengthLong(); + } + + // Ignored for compatibility on java6 + // @Override + public long getHeaderFieldLong(String name, long Default) { + return delegate.getHeaderFieldInt(name, (int) Default); + // Ignored for compatibility on java6 + // return delegate.getHeaderFieldLong(name, Default); + } + + // Ignored for compatibility on java6 + // @Override + public void setFixedLengthStreamingMode(long contentLength) { + // Ignored for compatibility on java6 + // delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override + public String getHeaderFieldKey(int n) { + return delegate.getHeaderFieldKey(n); + } + + @Override + public String getHeaderField(int n) { + return delegate.getHeaderField(n); + } + + @Override + public void connect() throws IOException { + delegate.connect(); + } + + @Override + public boolean getInstanceFollowRedirects() { + return delegate.getInstanceFollowRedirects(); + } + + @Override + public int getConnectTimeout() { + return delegate.getConnectTimeout(); + } + + @Override + public int getContentLength() { + return delegate.getContentLength(); + } + + @Override + public String getContentType() { + return delegate.getContentType(); + } + + @Override + public long getHeaderFieldDate(String name, long Default) { + return delegate.getHeaderFieldDate(name, Default); + } + + @Override + public String getContentEncoding() { + return delegate.getContentEncoding(); + } + + @Override + public void disconnect() { + delegate.disconnect(); + } + + @Override + public long getExpiration() { + return delegate.getExpiration(); + } + + @Override + public long getDate() { + return delegate.getDate(); + } + + @Override + public InputStream getErrorStream() { + return delegate.getErrorStream(); + } + + @Override + public long getLastModified() { + return delegate.getLastModified(); + } + + @Override + public String getHeaderField(String name) { + return delegate.getHeaderField(name); + } + + @Override + public Map<String, List<String>> getHeaderFields() { + return delegate.getHeaderFields(); + } + + @Override + public int getHeaderFieldInt(String name, int Default) { + return delegate.getHeaderFieldInt(name, Default); + } + + @Override + public Object getContent() throws IOException { + return delegate.getContent(); + } + + @Override + public Object getContent(@SuppressWarnings("rawtypes") Class[] classes) throws IOException { + return delegate.getContent(classes); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public boolean getDoInput() { + return delegate.getDoInput(); + } + + @Override + public boolean getDoOutput() { + return delegate.getDoOutput(); + } + + @Override + public boolean getAllowUserInteraction() { + return delegate.getAllowUserInteraction(); + } + + @Override + public void addRequestProperty(String key, String value) { + delegate.addRequestProperty(key, value); + } + + @Override + public String getCipherSuite() { + return delegate.getCipherSuite(); + } + + @Override + public boolean getDefaultUseCaches() { + return delegate.getDefaultUseCaches(); + } + + @Override + public HostnameVerifier getHostnameVerifier() { + return delegate.getHostnameVerifier(); + } + + @Override + public long getIfModifiedSince() { + return delegate.getIfModifiedSince(); + } + + @Override + public Certificate[] getLocalCertificates() { + return delegate.getLocalCertificates(); + } + + @Override + public Principal getLocalPrincipal() { + return delegate.getLocalPrincipal(); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setChunkedStreamingMode(int chunklen) { + delegate.setChunkedStreamingMode(chunklen); + } + + @Override + public void setConnectTimeout(int timeout) { + delegate.setConnectTimeout(timeout); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return delegate.getPeerPrincipal(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + delegate.setInstanceFollowRedirects(followRedirects); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + delegate.setRequestMethod(method); + } + + @Override + public String getRequestMethod() { + return delegate.getRequestMethod(); + } + + @Override + public void setReadTimeout(int timeout) { + delegate.setReadTimeout(timeout); + } + + @Override + public int getReadTimeout() { + return delegate.getReadTimeout(); + } + + @Override + public URL getURL() { + return delegate.getURL(); + } + + @Override + public boolean usingProxy() { + return delegate.usingProxy(); + } + + @Override + public Permission getPermission() throws IOException { + return delegate.getPermission(); + } + + @Override + public void setDoInput(boolean doinput) { + delegate.setDoInput(doinput); + } + + @Override + public void setDoOutput(boolean dooutput) { + delegate.setDoOutput(dooutput); + } + + @Override + public void setAllowUserInteraction(boolean allowuserinteraction) { + delegate.setAllowUserInteraction(allowuserinteraction); + } + + @Override + public void setUseCaches(boolean usecaches) { + delegate.setUseCaches(usecaches); + } + + @Override + public boolean getUseCaches() { + return delegate.getUseCaches(); + } + + @Override + public void setIfModifiedSince(long ifmodifiedsince) { + delegate.setIfModifiedSince(ifmodifiedsince); + } + + @Override + public void setDefaultUseCaches(boolean defaultusecaches) { + delegate.setDefaultUseCaches(defaultusecaches); + } + + @Override + public String getRequestProperty(String key) { + return delegate.getRequestProperty(key); + } + + @Override + public Map<String, List<String>> getRequestProperties() { + return delegate.getRequestProperties(); + } + + @Override + public SSLSocketFactory getSSLSocketFactory() { + return delegate.getSSLSocketFactory(); + } + + @Override + public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { + return delegate.getServerCertificates(); + } + + @Override + public void setHostnameVerifier(HostnameVerifier v) { + delegate.setHostnameVerifier(v); + } + + @Override + public void setRequestProperty(String key, String value) { + delegate.setRequestProperty(key, value); + } + + @Override + public void setSSLSocketFactory(SSLSocketFactory sf) { + delegate.setSSLSocketFactory(sf); + } + }; + } + return result; + } + + private WebTarget createNonRoutableTarget() { + Client client = ClientBuilder.newClient(); + client.property(ClientProperties.CONNECT_TIMEOUT, TimeoutBASE); + // the following address should not be routable, connections will timeout + return client.target("http://10.255.255.254/"); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/JerseyClientBuilderTest.java b/core-client/src/test/java/org/glassfish/jersey/client/JerseyClientBuilderTest.java new file mode 100644 index 0000000..b234e61 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/JerseyClientBuilderTest.java
@@ -0,0 +1,226 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.Response; + +import javax.annotation.Priority; +import javax.net.ssl.SSLContext; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * {@link JerseyClient} unit test. + * + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Michal Gajdos + */ +public class JerseyClientBuilderTest { + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + private JerseyClientBuilder builder; + + @Before + public void setUp() { + builder = new JerseyClientBuilder(); + } + + @Test + public void testBuildClientWithNullSslConfig() throws KeyStoreException { + try { + builder.sslContext(null); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + + try { + builder.keyStore(null, "abc"); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + try { + builder.keyStore(null, "abc".toCharArray()); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + try { + builder.keyStore(ks, (String) null); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + try { + builder.keyStore(ks, (char[]) null); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + + try { + builder.keyStore(null, (String) null); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + try { + builder.keyStore(null, (char[]) null); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + + try { + builder.trustStore(null); + fail("NullPointerException expected for 'null' SSL context."); + } catch (NullPointerException npe) { + // pass + } + } + + @Test + public void testOverridingSslConfig() throws KeyStoreException, NoSuchAlgorithmException { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + SSLContext ctx = SSLContext.getInstance("SSL"); + Client client; + + client = new JerseyClientBuilder().keyStore(ks, "qwerty").sslContext(ctx).build(); + assertSame("SSL context not the same as set in the client builder.", ctx, client.getSslContext()); + + client = new JerseyClientBuilder().sslContext(ctx).trustStore(ks).build(); + assertNotSame("SSL context not overridden in the client builder.", ctx, client.getSslContext()); + } + + @Priority(2) + public static class AbortingClientFilter implements ClientRequestFilter { + + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok("ok").build()); + } + } + + @Priority(1) + public static class ClientCreatingFilter implements ClientRequestFilter { + + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + if (Boolean.valueOf(requestContext.getHeaderString("create"))) { + assertThat(requestContext.getProperty("foo").toString(), equalTo("rab")); + + final Client client = ClientBuilder.newBuilder().withConfig(requestContext.getConfiguration()).build(); + final Response response = client.target("http://localhost").request().header("create", false).get(); + + requestContext.abortWith(response); + } else { + assertThat(requestContext.getConfiguration().getProperty("foo").toString(), equalTo("bar")); + } + } + } + + public static class ClientFeature implements Feature { + + @Override + public boolean configure(final FeatureContext context) { + if (context.getConfiguration().isRegistered(AbortingClientFilter.class)) { + throw new RuntimeException("Already Configured!"); + } + + context.register(ClientCreatingFilter.class); + context.register(AbortingClientFilter.class); + + context.property("foo", "bar"); + + return true; + } + } + + @Test + public void testCreateClientWithConfigFromClient() throws Exception { + _testCreateClientWithAnotherConfig(false); + } + + @Test + public void testCreateClientWithConfigFromRequestContext() throws Exception { + _testCreateClientWithAnotherConfig(true); + } + + public void _testCreateClientWithAnotherConfig(final boolean clientInFilter) throws Exception { + final Client client = ClientBuilder.newBuilder().register(new ClientFeature()).build(); + Response response = client.target("http://localhost") + .request().property("foo", "rab").header("create", clientInFilter).get(); + + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.readEntity(String.class), equalTo("ok")); + + final Client newClient = ClientBuilder.newClient(client.getConfiguration()); + response = newClient.target("http://localhost") + .request().property("foo", "rab").header("create", clientInFilter).get(); + + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.readEntity(String.class), equalTo("ok")); + + final Client newClientFromBuilder = ClientBuilder.newBuilder().withConfig(client.getConfiguration()).build(); + response = newClientFromBuilder.target("http://localhost") + .request().property("foo", "rab").header("create", clientInFilter).get(); + + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.readEntity(String.class), equalTo("ok")); + } + + @Test + public void testNegativeConnectTimeout() { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + + exception.expect(IllegalArgumentException.class); + clientBuilder.connectTimeout(-1, TimeUnit.SECONDS); + } + + @Test + public void testNegativeReadTimeout() { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + + exception.expect(IllegalArgumentException.class); + clientBuilder.readTimeout(-1, TimeUnit.SECONDS); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/JerseyClientTest.java b/core-client/src/test/java/org/glassfish/jersey/client/JerseyClientTest.java new file mode 100644 index 0000000..a3aa578 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/JerseyClientTest.java
@@ -0,0 +1,321 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.util.concurrent.Future; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.ReaderInterceptorContext; + +import javax.inject.Inject; + +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.internal.Version; +import org.glassfish.jersey.internal.inject.AbstractBinder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * {@link JerseyClient} unit test. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JerseyClientTest { + + private JerseyClient client; + + public JerseyClientTest() { + } + + @Before + public void setUp() { + this.client = (JerseyClient) ClientBuilder.newClient(); + } + + @After + public void tearDown() { + } + + @Test + public void testCreateClient() { + assertNotNull(client); + } + + @Test + public void testClose() { + client.close(); + assertTrue(client.isClosed()); + client.close(); // closing multiple times is ok + + try { + client.getConfiguration(); + fail("IllegalStateException expected if a method is called on a closed client instance."); + } catch (IllegalStateException ex) { + // ignored + } + try { + client.target("http://jersey.java.net/examples"); + fail("IllegalStateException expected if a method is called on a closed client instance."); + } catch (IllegalStateException ex) { + // ignored + } + } + + @Test + public void testConfiguration() { + final ClientConfig configuration = client.getConfiguration(); + assertNotNull(configuration); + + configuration.property("hello", "world"); + + assertEquals("world", client.getConfiguration().getProperty("hello")); + } + + @Test + public void testTarget() { + final JerseyWebTarget target = client.target("http://jersey.java.net/examples"); + assertNotNull(target); + assertEquals(client.getConfiguration(), target.getConfiguration()); + } + + @Test(expected = IllegalArgumentException.class) + public void testTargetIAE() { + final UriBuilder uriBuilder = UriBuilder.fromUri(":xxx:8080//yyy:90090//jaxrs "); + } + + public static class TestProvider implements ReaderInterceptor { + + @Override + public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException { + return context.proceed(); + } + } + + // Reproducer JERSEY-1637 + @Test + public void testRegisterNullOrEmptyContracts() { + final TestProvider provider = new TestProvider(); + + client.register(TestProvider.class, (Class<?>[]) null); + assertFalse(client.getConfiguration().isRegistered(TestProvider.class)); + + client.register(provider, (Class<?>[]) null); + assertFalse(client.getConfiguration().isRegistered(TestProvider.class)); + assertFalse(client.getConfiguration().isRegistered(provider)); + + client.register(TestProvider.class, new Class[0]); + assertFalse(client.getConfiguration().isRegistered(TestProvider.class)); + + client.register(provider, new Class[0]); + assertFalse(client.getConfiguration().isRegistered(TestProvider.class)); + assertFalse(client.getConfiguration().isRegistered(provider)); + } + + @Test + public void testTargetConfigUpdate() { + final JerseyWebTarget target = client.target("http://jersey.java.net/examples"); + + target.getConfiguration().register(new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext clientRequestContext) throws IOException { + throw new UnsupportedOperationException("Not supported yet"); + } + }); + + assertEquals(1, target.getConfiguration().getInstances().size()); + } + + /** + * Regression test for JERSEY-1192. + */ + @Test + public void testCreateLinkBasedInvocation() { + final JerseyClient jerseyClient = new JerseyClient(); + + try { + jerseyClient.invocation(null); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // success. + } + + try { + jerseyClient.invocation(null); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // success. + } + + Link link1 = + Link.fromUri(UriBuilder.fromPath("http://localhost:8080/").build()) + .build(); + Link link2 = + Link.fromUri(UriBuilder.fromPath("http://localhost:8080/").build()) + .type("text/plain") + .build(); + + + assertNotNull(jerseyClient.invocation(link1).buildPost(null)); + assertNotNull(jerseyClient.invocation(link2).buildPost(null)); + + assertNotNull(jerseyClient.invocation(link1).buildPost(Entity.text("Test."))); + assertNotNull(jerseyClient.invocation(link2).buildPost(Entity.text("Test."))); + + assertNotNull(jerseyClient.invocation(link1).buildPost(Entity.xml("Test."))); + assertNotNull(jerseyClient.invocation(link2).buildPost(Entity.xml("Test."))); + } + + @Test + public void userAgentTest() { + final Client customClient = ClientBuilder.newClient(new ClientConfig().connectorProvider(new TestConnector())); + + try { + customClient.target("test").request().get(); + } catch (ProcessingException e) { + assertEquals("Jersey/" + Version.getVersion(), e.getMessage()); + } + + try { + customClient.target("test").request().async().get().get(); + } catch (Exception e) { + assertEquals("Jersey/" + Version.getVersion(), e.getCause().getMessage()); + } + } + + /** + * JERSEY-2189 reproducer. + */ + @Test + public void customUserAgentTest() { + final Client customClient = ClientBuilder.newClient(new ClientConfig().connectorProvider(new TestConnector())); + + try { + customClient.target("test").request().header(HttpHeaders.USER_AGENT, null).get(); + } catch (Exception e) { + assertEquals("[null]", e.getMessage()); + } + + try { + customClient.target("test").request().header(HttpHeaders.USER_AGENT, null).async().get(); + } catch (Exception e) { + assertEquals("[null]", e.getCause().getMessage()); + } + + try { + customClient.target("test").request().header(HttpHeaders.USER_AGENT, "custom").get(); + } catch (Exception e) { + assertEquals("custom", e.getMessage()); + } + + try { + customClient.target("test").request().header(HttpHeaders.USER_AGENT, "custom").async().get(); + } catch (Exception e) { + assertEquals("custom", e.getCause().getMessage()); + } + } + + public static interface CustomContract { + public String getFoo(); + } + + public static class CustomService implements CustomContract { + + @Override + public String getFoo() { + return "Foo"; + } + } + + public static class CustomBinder extends AbstractBinder { + + @Override + protected void configure() { + bind(CustomService.class).to(CustomContract.class); + } + } + + public static class CustomProvider implements ClientRequestFilter { + @Inject + private CustomContract customContract; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok(customContract.getFoo()).build()); + } + } + + @Test + public void testCustomBinders() { + final CustomBinder binder = new CustomBinder(); + Client client = ClientBuilder.newClient().register(binder).register(CustomProvider.class); + + Response resp = client.target("test").request().get(); + assertEquals("Foo", resp.readEntity(String.class)); + } + + private static class TestConnector implements Connector, ConnectorProvider { + @Override + public ClientResponse apply(ClientRequest request) throws ProcessingException { + final Object agent = request.getHeaders().getFirst(HttpHeaders.USER_AGENT); + throw new ProcessingException((agent == null) ? "[null]" : agent.toString()); + } + + @Override + public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) { + final Object agent = request.getHeaders().getFirst(HttpHeaders.USER_AGENT); + callback.failure(new ProcessingException((agent == null) ? "[null]" : agent.toString())); + return null; + } + + @Override + public void close() { + // nothing + } + + @Override + public String getName() { + return null; + } + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return this; + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/JerseyCompletionStageRxInvokerTest.java b/core-client/src/test/java/org/glassfish/jersey/client/JerseyCompletionStageRxInvokerTest.java new file mode 100644 index 0000000..56d763f --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/JerseyCompletionStageRxInvokerTest.java
@@ -0,0 +1,182 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.CompletionStageRxInvoker; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder; +import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler; + +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; + +/** + * @author Pavel Bucek (pavel.bucek at oracle.com) + */ +public class JerseyCompletionStageRxInvokerTest { + + private Client client; + private ExecutorService executor; + + @Before + public void setUp() throws Exception { + client = ClientBuilder.newClient().register(TerminalClientRequestFilter.class); + executor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder() + .setNameFormat("jersey-rx-client-test-%d") + .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler()) + .build()); + } + + @After + public void tearDown() throws Exception { + executor.shutdown(); + client.close(); + client = null; + } + + @Test + public void testNewClient() throws Exception { + testClient(ClientBuilder.newClient().register(TerminalClientRequestFilter.class), false); + } + + @Test + @Ignore("TODO JAX-RS 2.1") + public void testNewClientExecutor() throws Exception { + testClient(ClientBuilder.newBuilder() + .executorService(executor) + .build() + .register(TerminalClientRequestFilter.class), true); + } + + @Test + public void testNotFoundResponse() throws Exception { + CompletionStageRxInvoker invoker = client.target("http://jersey.java.net") + .request() + .header("Response-Status", 404) + .rx(); + + testInvoker(invoker, 404, false); + } + + @Test(expected = NotFoundException.class) + public void testNotFoundReadEntityViaClass() throws Throwable { + try { + client.target("http://jersey.java.net") + .request() + .header("Response-Status", 404) + .rx() + .get(String.class) + .toCompletableFuture() + .get(); + } catch (final Exception expected) { + // java.util.concurrent.ExecutionException + throw expected + // javax.ws.rs.NotFoundException + .getCause(); + } + } + + @Test(expected = NotFoundException.class) + public void testNotFoundReadEntityViaGenericType() throws Throwable { + try { + client.target("http://jersey.java.net") + .request() + .header("Response-Status", 404) + .rx() + .get(new GenericType<String>() { + }) + .toCompletableFuture() + .get(); + } catch (final Exception expected) { + // java.util.concurrent.ExecutionException + throw expected + // javax.ws.rs.NotFoundException + .getCause(); + } + } + + @Test + public void testReadEntityViaClass() throws Throwable { + final String response = client.target("http://jersey.java.net") + .request() + .rx() + .get(String.class) + .toCompletableFuture() + .get(); + + assertThat(response, is("NO-ENTITY")); + } + + @Test + public void testReadEntityViaGenericType() throws Throwable { + final String response = client.target("http://jersey.java.net") + .request() + .rx() + .get(new GenericType<String>() { }) + .toCompletableFuture() + .get(); + + assertThat(response, is("NO-ENTITY")); + } + + private void testClient(final Client rxClient, final boolean testDedicatedThread) + throws Exception { + testTarget(rxClient.target("http://jersey.java.net"), testDedicatedThread); + } + + private void testTarget(final WebTarget rxTarget, boolean dedicatedThread) + throws Exception { + + testInvoker(rxTarget.request().rx(), 200, dedicatedThread); + } + + private void testInvoker(final CompletionStageRxInvoker rx, + final int expectedStatus, + final boolean testDedicatedThread) throws Exception { + + testResponse(rx.get().toCompletableFuture().get(), expectedStatus, testDedicatedThread); + } + + private static void testResponse(final Response response, final int expectedStatus, final boolean testDedicatedThread) { + assertThat(response.getStatus(), is(expectedStatus)); + assertThat(response.readEntity(String.class), is("NO-ENTITY")); + + // Executor. + final Matcher<String> matcher = containsString("jersey-rx-client-test"); + assertThat(response.getHeaderString("Test-Thread"), testDedicatedThread ? matcher : not(matcher)); + + // Properties. + assertThat(response.getHeaderString("Test-Uri"), is("http://jersey.java.net")); + assertThat(response.getHeaderString("Test-Method"), is("GET")); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/JerseyInvocationTest.java b/core-client/src/test/java/org/glassfish/jersey/client/JerseyInvocationTest.java new file mode 100644 index 0000000..dc021a3 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/JerseyInvocationTest.java
@@ -0,0 +1,400 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.ProtocolException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.AsyncInvoker; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.hamcrest.CoreMatchers; +import org.junit.Test; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Martin Matula + */ +public class JerseyInvocationTest { + + /** + * Regression test for JERSEY-1257 + */ + @Test + public void testOverrideHeadersWithMap() { + final MultivaluedMap<String, Object> map = new MultivaluedHashMap<String, Object>(); + map.add("a-header", "b-header"); + final JerseyInvocation invocation = buildInvocationWithHeaders(map); + assertEquals(1, invocation.request().getHeaders().size()); + assertEquals("b-header", invocation.request().getHeaders().getFirst("a-header")); + } + + /** + * Regression test for JERSEY-1257 + */ + @Test + public void testClearHeaders() { + final JerseyInvocation invocation = buildInvocationWithHeaders(null); + assertTrue(invocation.request().getHeaders().isEmpty()); + } + + /** + * Regression test for JERSEY-2562. + */ + @Test + public void testClearHeader() { + final Client client = ClientBuilder.newClient(); + final Invocation.Builder builder = client.target("http://localhost:8080/mypath").request(); + final JerseyInvocation invocation = (JerseyInvocation) builder + .header("foo", "bar").header("foo", null).header("bar", "foo") + .buildGet(); + final MultivaluedMap<String, Object> headers = invocation.request().getHeaders(); + + assertThat(headers.size(), is(1)); + assertThat(headers.keySet(), hasItem("bar")); + } + + private JerseyInvocation buildInvocationWithHeaders(final MultivaluedMap<String, Object> headers) { + final Client c = ClientBuilder.newClient(); + final Invocation.Builder builder = c.target("http://localhost:8080/mypath").request(); + return (JerseyInvocation) builder.header("unexpected-header", "unexpected-header").headers(headers).buildGet(); + } + + /** + * Checks that presence of request entity fo HTTP DELETE method does not fail in Jersey. + * Instead, the request is propagated up to HttpURLConnection, where it fails with + * {@code ProtocolException}. + * <p/> + * See also JERSEY-1711. + * + * @see #overrideHttpMethodBasedComplianceCheckNegativeTest() + */ + @Test + public void overrideHttpMethodBasedComplianceCheckTest() { + final Client c1 = ClientBuilder.newClient().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true); + try { + c1.target("http://localhost:8080/myPath").request().method("DELETE", Entity.text("body")); + fail("ProcessingException expected."); + } catch (final ProcessingException ex) { + assertThat(ex.getCause().getClass(), anyOf(CoreMatchers.<Class<?>>equalTo(ProtocolException.class), + CoreMatchers.<Class<?>>equalTo(ConnectException.class))); + } + + final Client c2 = ClientBuilder.newClient(); + try { + c2.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, + true).method("DELETE", Entity.text("body")); + fail("ProcessingException expected."); + } catch (final ProcessingException ex) { + assertThat(ex.getCause().getClass(), anyOf(CoreMatchers.<Class<?>>equalTo(ProtocolException.class), + CoreMatchers.<Class<?>>equalTo(ConnectException.class))); + } + + final Client c3 = ClientBuilder.newClient().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, false); + try { + c3.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, + true).method("DELETE", Entity.text("body")); + fail("ProcessingException expected."); + } catch (final ProcessingException ex) { + assertThat(ex.getCause().getClass(), anyOf(CoreMatchers.<Class<?>>equalTo(ProtocolException.class), + CoreMatchers.<Class<?>>equalTo(ConnectException.class))); + } + } + + /** + * Checks that presence of request entity fo HTTP DELETE method fails in Jersey with {@code IllegalStateException} + * if HTTP spec compliance is not suppressed by {@link ClientProperties#SUPPRESS_HTTP_COMPLIANCE_VALIDATION} property. + * <p/> + * See also JERSEY-1711. + * + * @see #overrideHttpMethodBasedComplianceCheckTest() + */ + @Test + public void overrideHttpMethodBasedComplianceCheckNegativeTest() { + final Client c1 = ClientBuilder.newClient(); + try { + c1.target("http://localhost:8080/myPath").request().method("DELETE", Entity.text("body")); + fail("IllegalStateException expected."); + } catch (final IllegalStateException expected) { + // pass + } + + final Client c2 = ClientBuilder.newClient(); + try { + c2.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, + false).method("DELETE", Entity.text("body")); + fail("IllegalStateException expected."); + } catch (final IllegalStateException expected) { + // pass + } + + final Client c3 = ClientBuilder.newClient().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true); + try { + c3.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, + false).method("DELETE", Entity.text("body")); + fail("IllegalStateException expected."); + } catch (final IllegalStateException expected) { + // pass + } + } + + @Test + public void testNullResponseType() throws Exception { + final Client client = ClientBuilder.newClient(); + client.register(new ClientRequestFilter() { + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok().build()); + } + }); + + final WebTarget target = client.target("http://localhost:8080/mypath"); + + final Class<Response> responseType = null; + final String[] methods = new String[] {"GET", "PUT", "POST", "DELETE", "OPTIONS"}; + + for (final String method : methods) { + final Invocation.Builder request = target.request(); + + try { + request.method(method, responseType); + fail("IllegalArgumentException expected."); + } catch (final IllegalArgumentException iae) { + // OK. + } + + final Invocation build = "PUT".equals(method) ? request.build(method, Entity.entity("", + MediaType.TEXT_PLAIN_TYPE)) : request.build(method); + + try { + build.submit(responseType); + fail("IllegalArgumentException expected."); + } catch (final IllegalArgumentException iae) { + // OK. + } + + try { + build.invoke(responseType); + fail("IllegalArgumentException expected."); + } catch (final IllegalArgumentException iae) { + // OK. + } + + try { + request.async().method(method, responseType); + fail("IllegalArgumentException expected."); + } catch (final IllegalArgumentException iae) { + // OK. + } + } + } + + @Test + public void failedCallbackTest() throws InterruptedException { + final Invocation.Builder builder = ClientBuilder.newClient().target("http://localhost:888/").request(); + for (int i = 0; i < 1; i++) { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger ai = new AtomicInteger(0); + final InvocationCallback<String> callback = new InvocationCallback<String>() { + @Override + public void completed(final String arg0) { + try { + ai.set(ai.get() + 1); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(final Throwable throwable) { + try { + + int result = 10; + if (throwable instanceof ProcessingException) { + result += 100; + } + final Throwable ioe = throwable.getCause(); + if (ioe instanceof IOException) { + result += 1000; + } + ai.set(ai.get() + result); + } finally { + latch.countDown(); + } + } + }; + + final Invocation invocation = builder.buildGet(); + final Future<String> future = invocation.submit(callback); + try { + future.get(); + fail("future.get() should have failed."); + } catch (final ExecutionException e) { + final Throwable pe = e.getCause(); + assertTrue("Execution exception cause is not a ProcessingException: " + pe.toString(), + pe instanceof ProcessingException); + final Throwable ioe = pe.getCause(); + assertTrue("Execution exception cause is not an IOException: " + ioe.toString(), ioe instanceof IOException); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + + latch.await(1, TimeUnit.SECONDS); + + assertEquals(1110, ai.get()); + } + } + + public static class MyUnboundCallback<V> implements InvocationCallback<V> { + + private final CountDownLatch latch; + private volatile Throwable throwable; + + public MyUnboundCallback(final CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void completed(final V v) { + latch.countDown(); + } + + @Override + public void failed(final Throwable throwable) { + this.throwable = throwable; + latch.countDown(); + } + + public Throwable getThrowable() { + return throwable; + } + } + + @Test + public void failedUnboundGenericCallback() throws InterruptedException { + final Invocation invocation = ClientBuilder.newClient().target("http://localhost:888/").request().buildGet(); + final CountDownLatch latch = new CountDownLatch(1); + + final MyUnboundCallback<String> callback = new MyUnboundCallback<String>(latch); + invocation.submit(callback); + + latch.await(1, TimeUnit.SECONDS); + + assertThat(callback.getThrowable(), CoreMatchers.instanceOf(ProcessingException.class)); + assertThat(callback.getThrowable().getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class)); + assertThat(callback.getThrowable().getCause().getMessage(), CoreMatchers + .allOf(CoreMatchers.containsString(MyUnboundCallback.class.getName()), + CoreMatchers.containsString(InvocationCallback.class.getName()))); + } + + @Test + public void testSubmitWithGenericType() throws Exception { + _submitWithGenericType(new GenericType<String>() {}); + } + + @Test + public void testSubmitWithGenericTypeParam() throws Exception { + _submitWithGenericType(new GenericType(String.class) {}); + } + + private void _submitWithGenericType(final GenericType type) throws Exception { + final Invocation.Builder builder = ClientBuilder.newClient() + .register(TerminatingFilter.class) + .target("http://localhost/") + .request(); + + final AtomicReference<String> reference = new AtomicReference<String>(); + final CountDownLatch latch = new CountDownLatch(1); + + final InvocationCallback callback = new InvocationCallback<Object>() { + @Override + public void completed(final Object obj) { + reference.set(obj.toString()); + latch.countDown(); + } + + @Override + public void failed(final Throwable throwable) { + latch.countDown(); + } + }; + + //noinspection unchecked + ((JerseyInvocation) builder.buildGet()) + .submit(type, (InvocationCallback<String>) callback); + + latch.await(); + + assertThat(reference.get(), is("ENTITY")); + } + + public static class TerminatingFilter implements ClientRequestFilter { + + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok("ENTITY").build()); + } + } + + @Test + public void runtimeExceptionInAsyncInvocation() throws ExecutionException, InterruptedException { + final AsyncInvoker ai = ClientBuilder.newClient().register(new ExceptionInvokerFilter()) + .target("http://localhost:888/").request().async(); + + try { + ai.get().get(); + fail("ExecutionException should be thrown"); + } catch (ExecutionException ee) { + assertEquals(ProcessingException.class, ee.getCause().getClass()); + assertEquals(RuntimeException.class, ee.getCause().getCause().getClass()); + } + } + + public static class ExceptionInvokerFilter implements ClientRequestFilter { + + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + throw new RuntimeException("ExceptionInvokerFilter RuntimeException"); + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/JerseyWebTargetTest.java b/core-client/src/test/java/org/glassfish/jersey/client/JerseyWebTargetTest.java new file mode 100644 index 0000000..e3be2f9 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/JerseyWebTargetTest.java
@@ -0,0 +1,448 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.ReaderInterceptorContext; + +import org.glassfish.jersey.uri.internal.JerseyUriBuilder; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * {@code JerseyWebTarget} implementation unit tests. + * + * @author Martin Matula + */ +public class JerseyWebTargetTest { + private JerseyClient client; + private JerseyWebTarget target; + + @Before + public void setUp() { + this.client = (JerseyClient) ClientBuilder.newClient(); + this.target = client.target("/"); + } + + @Test + public void testClose() { + client.close(); + try { + target.getUriBuilder(); + fail("IllegalStateException was expected."); + } catch (IllegalStateException e) { + // ignore + } + try { + target.getConfiguration(); + fail("IllegalStateException was expected."); + } catch (IllegalStateException e) { + // ignore + } + } + + public static class TestProvider implements ReaderInterceptor { + + @Override + public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException { + return context.proceed(); + } + } + + // Reproducer JERSEY-1637 + @Test + public void testRegisterNullOrEmptyContracts() { + final TestProvider provider = new TestProvider(); + + target.register(TestProvider.class, (Class<?>[]) null); + assertFalse(target.getConfiguration().isRegistered(TestProvider.class)); + + target.register(provider, (Class<?>[]) null); + assertFalse(target.getConfiguration().isRegistered(TestProvider.class)); + assertFalse(target.getConfiguration().isRegistered(provider)); + + target.register(TestProvider.class, new Class[0]); + assertFalse(target.getConfiguration().isRegistered(TestProvider.class)); + + target.register(provider, new Class[0]); + assertFalse(target.getConfiguration().isRegistered(TestProvider.class)); + assertFalse(target.getConfiguration().isRegistered(provider)); + } + + @Test + public void testResolveTemplate() { + URI uri; + UriBuilder uriBuilder; + + uri = target.resolveTemplate("a", "v").getUri(); + assertEquals("/", uri.toString()); + + uri = target.path("{a}").resolveTemplate("a", "v").getUri(); + assertEquals("/v", uri.toString()); + + uriBuilder = target.path("{a}").resolveTemplate("qqq", "qqq").getUriBuilder(); + assertEquals("/{a}", uriBuilder.toTemplate()); + + uriBuilder = target.path("{a}").resolveTemplate("a", "v").resolveTemplate("a", "x").getUriBuilder(); + assertEquals("/v", uriBuilder.build().toString()); + + try { + target.resolveTemplate(null, null); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // expected + } + } + + @Test + public void testResolveTemplate2() { + final JerseyWebTarget newTarget = target.path("path/{a}").queryParam("query", "{q}").resolveTemplate("a", "param-a"); + final JerseyUriBuilder uriBuilder = (JerseyUriBuilder) newTarget.getUriBuilder(); + uriBuilder.resolveTemplate("q", "param-q").resolveTemplate("a", "will-be-ignored"); + assertEquals(URI.create("/path/param-a?query=param-q"), uriBuilder.build()); + + final UriBuilder uriBuilderNew = newTarget.resolveTemplate("a", "will-be-ignored").resolveTemplate("q", + "new-q").getUriBuilder(); + assertEquals(URI.create("/path/param-a?query=new-q"), uriBuilderNew.build()); + } + + @Test + public void testResolveTemplate3() { + final JerseyWebTarget webTarget = target.path("path/{a}").path("{b}").queryParam("query", "{q}") + .resolveTemplate("a", "param-a").resolveTemplate("q", "param-q"); + assertEquals("/path/param-a/{b}?query=param-q", webTarget.getUriBuilder().toTemplate()); + // resolve b in webTarget + assertEquals(URI.create("/path/param-a/param-b?query=param-q"), webTarget.resolveTemplate("b", + "param-b").getUri()); + + // check that original webTarget has not been changed + assertEquals("/path/param-a/{b}?query=param-q", webTarget.getUriBuilder().toTemplate()); + + // resolve b in UriBuilder + assertEquals(URI.create("/path/param-a/param-b?query=param-q"), ((JerseyUriBuilder) webTarget.getUriBuilder()) + .resolveTemplate("b", "param-b").build()); + + // resolve in build method + assertEquals(URI.create("/path/param-a/param-b?query=param-q"), ((JerseyUriBuilder) webTarget.getUriBuilder()) + .build("param-b")); + } + + + @Test + public void testResolveTemplateFromEncoded() { + final String a = "a%20%3F/*/"; + final String b = "/b/"; + assertEquals("/path/a%20%3F/*///b/", target.path("path/{a}/{b}").resolveTemplateFromEncoded("a", + a).resolveTemplateFromEncoded("b", b).getUri().toString()); + assertEquals("/path/a%2520%253F%2F*%2F/%2Fb%2F", target.path("path/{a}/{b}").resolveTemplate("a", + a).resolveTemplate("b", b).getUri().toString()); + assertEquals("/path/a%2520%253F/*///b/", target.path("path/{a}/{b}").resolveTemplate("a", + a, false).resolveTemplate("b", b, false).getUri().toString()); + } + + + @Test + public void testResolveTemplatesFromEncoded() { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("a", "a%20%3F/*/"); + map.put("b", "/b/"); + + assertEquals("/path/a%20%3F/*///b/", target.path("path/{a}/{b}").resolveTemplatesFromEncoded(map).getUri() + .toString()); + assertEquals("/path/a%2520%253F%2F*%2F/%2Fb%2F", target.path("path/{a}/{b}").resolveTemplates(map).getUri() + .toString()); + assertEquals("/path/a%2520%253F/*///b/", target.path("path/{a}/{b}").resolveTemplates(map, + false).getUri().toString()); + + List<Map<String, Object>> corruptedTemplateValuesList = Arrays.asList( + null, + new HashMap<String, Object>() {{ + put(null, "value"); + }}, + new HashMap<String, Object>() {{ + put("name", null); + }}, + new HashMap<String, Object>() {{ + put("a", "foo"); + put("name", null); + }}, + new HashMap<String, Object>() {{ + put("name", null); + put("a", "foo"); + }} + ); + + for (final Map<String, Object> corruptedTemplateValues : corruptedTemplateValuesList) { + try { + target.path("path/{a}/{b}").resolveTemplatesFromEncoded(corruptedTemplateValues); + fail("NullPointerException expected. " + corruptedTemplateValues); + } catch (NullPointerException ex) { + // expected + } catch (Exception e) { + fail("NullPointerException expected for template values " + corruptedTemplateValues + ", caught: " + e); + } + } + + for (final Map<String, Object> corruptedTemplateValues : corruptedTemplateValuesList) { + try { + target.path("path/{a}/{b}").resolveTemplates(corruptedTemplateValues); + fail("NullPointerException expected. " + corruptedTemplateValues); + } catch (NullPointerException ex) { + // expected + } catch (Exception e) { + fail("NullPointerException expected for template values " + corruptedTemplateValues + ", caught: " + e); + } + } + + for (final Map<String, Object> corruptedTemplateValues : corruptedTemplateValuesList) { + for (final boolean encode : new boolean[]{true, false}) { + try { + target.path("path/{a}/{b}").resolveTemplates(corruptedTemplateValues, encode); + fail("NullPointerException expected. " + corruptedTemplateValues); + } catch (NullPointerException ex) { + // expected + } catch (Exception e) { + fail("NullPointerException expected for template values " + corruptedTemplateValues + ", caught: " + e); + } + } + } + } + + + @Test + public void testGetUriBuilder() { + final Map<String, Object> params = new HashMap<String, Object>(2); + params.put("a", "w1"); + UriBuilder uriBuilder = target.path("{a}").resolveTemplate("a", "v1").resolveTemplates(params).getUriBuilder(); + assertEquals("/v1", uriBuilder.build().toString()); + } + + @Test + public void testQueryParams() { + URI uri; + + uri = target.path("a").queryParam("q", "v1", "v2").queryParam("q").getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").queryParam("q", "v1", "v2").queryParam("q", (Object) null).getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").queryParam("q", "v1", "v2").queryParam("q", (Object[]) null).getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").queryParam("q", "v1", "v2").queryParam("q", new Object[]{}).getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").queryParam("q", "v").getUri(); + assertEquals("/a?q=v", uri.toString()); + + uri = target.path("a").queryParam("q1", "v1").queryParam("q2", "v2").queryParam("q1", (Object) null).getUri(); + assertEquals("/a?q2=v2", uri.toString()); + + try { + target.queryParam("q", "v1", null, "v2", null); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // expected + } + + { + uri = target.path("a").queryParam("q1", "v1").queryParam("q2", "v2").queryParam("q1", "w1", "w2") + .queryParam("q2", (Object) null).getUri(); + assertEquals("/a?q1=v1&q1=w1&q1=w2", uri.toString()); + } + + try { + target.queryParam(null); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // expected + } + + try { + target.queryParam(null, "param"); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // expected + } + + try { + target.path("a").queryParam("q1", "v1").queryParam("q2", "v2").queryParam("q1", "w1", null) + .queryParam("q2", (Object) null); + + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // expected + } + } + + @Test + public void testMatrixParams() { + URI uri; + + uri = target.path("a").matrixParam("q", "v1", "v2").matrixParam("q").getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").matrixParam("q", "v1", "v2").matrixParam("q", (Object) null).getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").matrixParam("q", "v1", "v2").matrixParam("q", (Object[]) null).getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").matrixParam("q", "v1", "v2").matrixParam("q", new Object[]{}).getUri(); + assertEquals("/a", uri.toString()); + + uri = target.path("a").matrixParam("q", "v").getUri(); + assertEquals("/a;q=v", uri.toString()); + + uri = target.path("a").matrixParam("q1", "v1").matrixParam("q2", "v2").matrixParam("q1", (Object) null).getUri(); + assertEquals("/a;q2=v2", uri.toString()); + + try { + target.matrixParam("q", "v1", null, "v2", null); + fail("NullPointerException expected."); + } catch (NullPointerException ex) { + // expected + } + } + + @Test + public void testRemoveMatrixParams() { + WebTarget wt = target; + wt = wt.matrixParam("matrix1", "segment1"); + wt = wt.path("path1"); + wt = wt.matrixParam("matrix2", "segment1"); + wt = wt.matrixParam("matrix2", new Object[]{null}); + wt = wt.path("path2"); + wt = wt.matrixParam("matrix1", "segment1"); + wt = wt.matrixParam("matrix1", new Object[]{null}); + wt = wt.path("path3"); + URI uri = wt.getUri(); + assertEquals("/;matrix1=segment1/path1/path2/path3", uri.toString()); + } + + @Test + public void testReplaceMatrixParam() { + WebTarget wt = target; + wt = wt.path("path1"); + wt = wt.matrixParam("matrix10", "segment10-delete"); + wt = wt.matrixParam("matrix11", "segment11"); + wt = wt.matrixParam("matrix10", new Object[]{null}); + wt = wt.path("path2"); + wt = wt.matrixParam("matrix20", "segment20-delete"); + wt = wt.matrixParam("matrix20", new Object[]{null}); + wt = wt.matrixParam("matrix20", "segment20-delete-again"); + wt = wt.matrixParam("matrix20", new Object[]{null}); + wt = wt.path("path3"); + wt = wt.matrixParam("matrix30", "segment30-delete"); + wt = wt.matrixParam("matrix30", new Object[]{null}); + wt = wt.matrixParam("matrix30", "segment30-delete-again"); + wt = wt.matrixParam("matrix30", new Object[]{null}); + wt = wt.matrixParam("matrix30", "segment30"); + wt = wt.path("path4"); + wt = wt.matrixParam("matrix40", "segment40-delete"); + wt = wt.matrixParam("matrix40", new Object[]{null}); + + URI uri = wt.getUri(); + assertEquals("/path1;matrix11=segment11/path2/path3;matrix30=segment30/path4", uri.toString()); + } + + @Test(expected = NullPointerException.class) + public void testQueryParamNull() { + WebTarget wt = target; + + wt.queryParam(null); + } + + @Test(expected = NullPointerException.class) + public void testPathNull() { + WebTarget wt = target; + + wt.path(null); + } + + @Test(expected = NullPointerException.class) + public void testResolveTemplateNull1() { + WebTarget wt = target; + + wt.resolveTemplate(null, "", true); + } + + @Test(expected = NullPointerException.class) + public void testResolveTemplateNull2() { + WebTarget wt = target; + + wt.resolveTemplate("name", null, true); + } + + @Test(expected = NullPointerException.class) + public void testResolveTemplateFromEncodedNull1() { + WebTarget wt = target; + + wt.resolveTemplateFromEncoded(null, ""); + } + + @Test(expected = NullPointerException.class) + public void testResolveTemplateFromEncodedNull2() { + WebTarget wt = target; + + wt.resolveTemplateFromEncoded("name", null); + } + + @Test + public void testResolveTemplatesEncodedEmptyMap() { + WebTarget wt = target; + wt = wt.resolveTemplatesFromEncoded(Collections.<String, Object>emptyMap()); + + assertEquals(target, wt); + } + + @Test + public void testResolveTemplatesEmptyMap() { + WebTarget wt = target; + wt = wt.resolveTemplates(Collections.<String, Object>emptyMap()); + + assertEquals(target, wt); + } + + @Test + public void testResolveTemplatesEncodeSlashEmptyMap() { + WebTarget wt = target; + wt = wt.resolveTemplates(Collections.<String, Object>emptyMap(), false); + + assertEquals(target, wt); + } + +} + +
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/LinkTest.java b/core-client/src/test/java/org/glassfish/jersey/client/LinkTest.java new file mode 100644 index 0000000..f7b6027 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/LinkTest.java
@@ -0,0 +1,61 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Link; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * LinkTest class. + * + * @author Santiago.Pericas-Geertsen (santiago.pericasgeertsen at oracle.com) + */ +public class LinkTest { + + private JerseyClient client; + + public LinkTest() { + } + + @Before + public void setUp() { + this.client = (JerseyClient) ClientBuilder.newClient(); + } + + @Test + public void testInvocationFromLinkNoEntity() { + Link l = Link.fromUri("http://examples.org/app").type("text/plain").build(); + assertNotNull(l); + + javax.ws.rs.client.Invocation i = client.invocation(l).buildGet(); + assertNotNull(i); + } + + @Test + public void testInvocationFromLinkWithEntity() { + Link l = Link.fromUri("http://examples.org/app").type("*/*").build(); + Entity<String> e = Entity.text("hello world"); + javax.ws.rs.client.Invocation i = client.invocation(l).buildPost(e); + assertTrue(i != null); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/ShutdownHookLeakTest.java b/core-client/src/test/java/org/glassfish/jersey/client/ShutdownHookLeakTest.java new file mode 100644 index 0000000..d92fec7 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/ShutdownHookLeakTest.java
@@ -0,0 +1,89 @@ +/* + * Copyright (c) 2015, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.util.Collection; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.number.OrderingComparison.lessThan; +import static org.junit.Assert.assertThat; + +/** + * Reproducer for JERSEY-2786. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class ShutdownHookLeakTest { + + private static final int ITERATIONS = 4000; + private static final int THRESHOLD = ITERATIONS * 2 / 3; + + @SuppressWarnings("unchecked") + @Test + public void testShutdownHookDoesNotLeak() throws Exception { + final Client client = ClientBuilder.newClient(); + final WebTarget target = client.target("http://example.com"); + + final Collection shutdownHooks = getShutdownHooks(client); + + for (int i = 0; i < ITERATIONS; i++) { + // Create/Initialize client runtime. + target.property("Washington", "Irving") + .request() + .property("how", "now") + .buildGet() + .property("Irving", "Washington"); + } + + System.gc(); + + int notEnqueued = 0; + int notNull = 0; + for (final Object o : shutdownHooks) { + if (((WeakReference<JerseyClient.ShutdownHook>) o).get() != null) { + notNull++; + } + if (!((WeakReference<JerseyClient.ShutdownHook>) o).isEnqueued()) { + notEnqueued++; + } + } + + assertThat( + "Non-null shutdown hook references count should not copy number of property invocation", + // 66 % seems like a reasonable threshold for this test to keep it stable + notNull, is(lessThan(THRESHOLD))); + + assertThat( + "Shutdown hook references count not enqueued in the ReferenceQueue should not copy number of property invocation", + // 66 % seems like a reasonable threshold for this test to keep it stable + notEnqueued, is(lessThan(THRESHOLD))); + } + + private Collection getShutdownHooks(final Client client) throws NoSuchFieldException, IllegalAccessException { + final JerseyClient jerseyClient = (JerseyClient) client; + final Field shutdownHooksField = JerseyClient.class.getDeclaredField("shutdownHooks"); + shutdownHooksField.setAccessible(true); + return (Collection) shutdownHooksField.get(jerseyClient); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/TerminalClientRequestFilter.java b/core-client/src/test/java/org/glassfish/jersey/client/TerminalClientRequestFilter.java new file mode 100644 index 0000000..dafb5af --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/TerminalClientRequestFilter.java
@@ -0,0 +1,78 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +/** + * Client request filter, which doesn't perform actual requests. + * + * Sets the response based on various request properties: + * <ul> + * <li>Response code is value of request header {@code "Response-Status"} or {@code 200}, if not present.</li> + * <li>Response entity is request entity or {@code "NO-ENTITY"}, if request entity is not present.</li> + * <li>Response headers do contain all request headers, names are prefixed by {@code "Test-Header-"}.</li> + * <li>Response headers do contain all request properties, names are prefixed by {@code "Test-Property-"}.</li> + * </ul> + * + * Additionally, response headers contain {@code "Test-Thread"} (current thread name), {@code "Test-Uri"} (request Uri) + * and {@code "Test-Method"} (request Http method). + * + * @author Michal Gajdos + */ +class TerminalClientRequestFilter implements ClientRequestFilter { + + @Override + public void filter(final ClientRequestContext requestContext) throws IOException { + // Obtain entity - from request or create new. + final ByteArrayInputStream entity = new ByteArrayInputStream( + requestContext.hasEntity() ? requestContext.getEntity().toString().getBytes() : "NO-ENTITY".getBytes() + ); + + final int responseStatus = Optional.ofNullable(requestContext.getHeaders().getFirst("Response-Status")) + .map(Integer.class::cast) + .orElse(200); + + Response.ResponseBuilder response = Response.status(responseStatus) + .entity(entity) + .type("text/plain") + // Test properties. + .header("Test-Thread", Thread.currentThread().getName()) + .header("Test-Uri", requestContext.getUri().toString()) + .header("Test-Method", requestContext.getMethod()); + + // Request headers -> Response headers (<header> -> Test-Header-<header>) + for (final MultivaluedMap.Entry<String, List<String>> entry : requestContext.getStringHeaders().entrySet()) { + response = response.header("Test-Header-" + entry.getKey(), entry.getValue()); + } + + // Request properties -> Response headers (<header> -> Test-Property-<header>) + for (final String property : requestContext.getPropertyNames()) { + response = response.header("Test-Property-" + property, requestContext.getProperty(property)); + } + + requestContext.abortWith(response.build()); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/WebTargetPropertiesTest.java b/core-client/src/test/java/org/glassfish/jersey/client/WebTargetPropertiesTest.java new file mode 100644 index 0000000..2fb4850 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/WebTargetPropertiesTest.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.junit.Test; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author pavel.bucek@oracle.com + */ +public class WebTargetPropertiesTest { + + @Test + public void testPropagation() { + Client c = ClientBuilder.newBuilder().newClient(); + + c.property("a", "val"); + + WebTarget w1 = c.target("http://a"); + w1.property("b", "val"); + + WebTarget w2 = w1.path("c"); + w2.property("c", "val"); + + assertTrue(c.getConfiguration().getProperties().containsKey("a")); + assertTrue(w1.getConfiguration().getProperties().containsKey("a")); + assertTrue(w2.getConfiguration().getProperties().containsKey("a")); + + assertFalse(c.getConfiguration().getProperties().containsKey("b")); + assertTrue(w1.getConfiguration().getProperties().containsKey("b")); + assertTrue(w2.getConfiguration().getProperties().containsKey("b")); + + assertFalse(c.getConfiguration().getProperties().containsKey("c")); + assertFalse(w1.getConfiguration().getProperties().containsKey("c")); + assertTrue(w2.getConfiguration().getProperties().containsKey("c")); + + w2.property("a", null); + + assertTrue(c.getConfiguration().getProperties().containsKey("a")); + assertTrue(w1.getConfiguration().getProperties().containsKey("a")); + assertFalse(w2.getConfiguration().getProperties().containsKey("a")); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/authentication/HttpDigestAuthFilterTest.java b/core-client/src/test/java/org/glassfish/jersey/client/authentication/HttpDigestAuthFilterTest.java new file mode 100644 index 0000000..284e87c --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/authentication/HttpDigestAuthFilterTest.java
@@ -0,0 +1,110 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.glassfish.jersey.client.authentication.DigestAuthenticator.DigestScheme; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Raphael Jolivet (raphael.jolivet at gmail.com) + * @author Stefan Katerkamp (stefan at katerkamp.de) + */ +public class HttpDigestAuthFilterTest { + + @Test + public void testParseHeaders1() throws Exception { // no digest scheme + final DigestAuthenticator f = new DigestAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"), 10000); + final Method method = DigestAuthenticator.class.getDeclaredMethod("parseAuthHeaders", List.class); + method.setAccessible(true); + final DigestScheme ds = (DigestScheme) method.invoke(f, + Arrays.asList(new String[] { + "basic toto=tutu", + "basic toto=\"tutu\"" + })); + + Assert.assertNull(ds); + } + + @Test + public void testParseHeaders2() throws Exception { // Two concurrent schemes + final DigestAuthenticator f = new DigestAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"), 10000); + final Method method = DigestAuthenticator.class.getDeclaredMethod("parseAuthHeaders", List.class); + method.setAccessible(true); + final DigestScheme ds = (DigestScheme) method.invoke(f, + Arrays.asList(new String[] { + "Digest realm=\"tata\"", + "basic toto=\"tutu\"" + })); + Assert.assertNotNull(ds); + + Assert.assertEquals("tata", ds.getRealm()); + } + + @Test + public void testParseHeaders3() throws Exception { // Complex case, with comma inside value + final DigestAuthenticator f = new DigestAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"), 10000); + final Method method = DigestAuthenticator.class.getDeclaredMethod("parseAuthHeaders", List.class); + method.setAccessible(true); + final DigestScheme ds = (DigestScheme) method.invoke(f, + Arrays.asList(new String[] { + "digest realm=\"tata\",nonce=\"foo, bar\"" + })); + + Assert.assertNotNull(ds); + Assert.assertEquals("tata", ds.getRealm()); + Assert.assertEquals("foo, bar", ds.getNonce()); + } + + @Test + public void testParseHeaders4() throws Exception { // Spaces + final DigestAuthenticator f = new DigestAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"), 10000); + final Method method = DigestAuthenticator.class.getDeclaredMethod("parseAuthHeaders", List.class); + method.setAccessible(true); + final DigestScheme ds = (DigestScheme) method.invoke(f, + Arrays.asList(new String[] { + " digest realm = \"tata\" , opaque=\"bar\" ,nonce=\"foo, bar\"" + })); + + Assert.assertNotNull(ds); + Assert.assertEquals("tata", ds.getRealm()); + Assert.assertEquals("foo, bar", ds.getNonce()); + Assert.assertEquals("bar", ds.getOpaque()); + } + + @Test + public void testParseHeaders5() throws Exception { // Mix of quotes and non-quotes + final DigestAuthenticator f = new DigestAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"), 10000); + final Method method = DigestAuthenticator.class.getDeclaredMethod("parseAuthHeaders", List.class); + method.setAccessible(true); + final DigestScheme ds = (DigestScheme) method.invoke(f, + Arrays.asList(new String[] { + " digest realm = \"tata\" , opaque =bar ,nonce=\"foo, bar\", algorithm=md5" + })); + + Assert.assertNotNull(ds); + Assert.assertEquals("tata", ds.getRealm()); + Assert.assertEquals("foo, bar", ds.getNonce()); + Assert.assertEquals("bar", ds.getOpaque()); + Assert.assertEquals("MD5", ds.getAlgorithm().name()); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/filter/ClientProviderInstanceInjectionTest.java b/core-client/src/test/java/org/glassfish/jersey/client/filter/ClientProviderInstanceInjectionTest.java new file mode 100644 index 0000000..8ec83fa --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/filter/ClientProviderInstanceInjectionTest.java
@@ -0,0 +1,109 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + +import java.io.IOException; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.Response; + +import javax.inject.Inject; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.InjectionManager; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Tests injections into provider instances. + * + * @author Miroslav Fuksa + * @author Michal Gajdos + */ +public class ClientProviderInstanceInjectionTest { + + public static class MyInjectee { + + private final String value; + + public MyInjectee(final String value) { + this.value = value; + } + + public String getSomething() { + return value; + } + } + + public static class MyInjecteeBinder extends AbstractBinder { + + @Override + protected void configure() { + bind(new MyInjectee("hello")); + } + } + + public static class MyFilter implements ClientRequestFilter { + + private final Object field; + + @Inject + private MyInjectee myInjectee; + + public MyFilter(Object field) { + this.field = field; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok(myInjectee + "," + field).build()); + } + } + + public static class MyFilterFeature implements Feature { + + @Inject + private InjectionManager injectionManager; + + @Override + public boolean configure(final FeatureContext context) { + context.register(new MyFilter(injectionManager)); + return true; + } + } + + /** + * Tests that instance of a feature or other provider will not be injected on the client-side. + */ + @Test + public void test() { + final Client client = ClientBuilder.newBuilder() + .register(new MyFilterFeature()) + .register(new MyInjecteeBinder()) + .build(); + final Response response = client.target("http://foo.bar").request().get(); + + assertEquals(200, response.getStatus()); + assertEquals("null,null", response.readEntity(String.class)); + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/filter/CsrfProtectionFilterTest.java b/core-client/src/test/java/org/glassfish/jersey/client/filter/CsrfProtectionFilterTest.java new file mode 100644 index 0000000..a69df03 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/filter/CsrfProtectionFilterTest.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + +import java.util.concurrent.Future; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Cross-site request forgery client filter test. + * + * @author Martin Matula + */ +public class CsrfProtectionFilterTest { + private Invocation.Builder invBuilder; + + @Before + public void setUp() { + Client client = ClientBuilder.newClient(new ClientConfig(CsrfProtectionFilter.class) + .connectorProvider(new TestConnector())); + invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + } + + @Test + public void testGet() { + Response r = invBuilder.get(); + assertNull(r.getHeaderString(CsrfProtectionFilter.HEADER_NAME)); + } + + @Test + public void testPut() { + Response r = invBuilder.put(Entity.entity("put", MediaType.TEXT_PLAIN_TYPE)); + assertNotNull(r.getHeaderString(CsrfProtectionFilter.HEADER_NAME)); + } + + private static class TestConnector implements Connector, ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return this; + } + + @Override + public ClientResponse apply(ClientRequest requestContext) { + final ClientResponse responseContext = new ClientResponse( + Response.Status.OK, requestContext); + + final String headerValue = requestContext.getHeaderString(CsrfProtectionFilter.HEADER_NAME); + if (headerValue != null) { + responseContext.header(CsrfProtectionFilter.HEADER_NAME, headerValue); + } + return responseContext; + } + + @Override + public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) { + throw new UnsupportedOperationException("Asynchronous execution not supported."); + } + + @Override + public void close() { + // do nothing + } + + @Override + public String getName() { + return null; + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/filter/EncodingFilterTest.java b/core-client/src/test/java/org/glassfish/jersey/client/filter/EncodingFilterTest.java new file mode 100644 index 0000000..66dfc48 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/filter/EncodingFilterTest.java
@@ -0,0 +1,237 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.concurrent.Future; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import static javax.ws.rs.core.HttpHeaders.ACCEPT_ENCODING; +import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.message.DeflateEncoder; +import org.glassfish.jersey.message.GZipEncoder; + +import org.junit.Ignore; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Client-side content encoding filter unit tests. + * + * @author Martin Matula + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class EncodingFilterTest { + @Test + public void testAcceptEncoding() { + Client client = ClientBuilder.newClient(new ClientConfig( + EncodingFilter.class, + GZipEncoder.class, + DeflateEncoder.class + ).connectorProvider(new TestConnector())); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.get(); + assertEquals("deflate,gzip,x-gzip", r.getHeaderString(ACCEPT_ENCODING)); + assertNull(r.getHeaderString(CONTENT_ENCODING)); + } + + @Test + public void testContentEncoding() { + Client client = ClientBuilder.newClient(new ClientConfig( + EncodingFilter.class, + GZipEncoder.class, + DeflateEncoder.class + ).property(ClientProperties.USE_ENCODING, "gzip").connectorProvider(new TestConnector())); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.post(Entity.entity("Hello world", MediaType.TEXT_PLAIN_TYPE)); + assertEquals("deflate,gzip,x-gzip", r.getHeaderString(ACCEPT_ENCODING)); + assertEquals("gzip", r.getHeaderString(CONTENT_ENCODING)); + } + + @Test + public void testContentEncodingViaFeature() { + Client client = ClientBuilder.newClient(new ClientConfig() + .connectorProvider(new TestConnector()) + .register(new EncodingFeature("gzip", GZipEncoder.class, DeflateEncoder.class))); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.post(Entity.entity("Hello world", MediaType.TEXT_PLAIN_TYPE)); + assertEquals("deflate,gzip,x-gzip", r.getHeaderString(ACCEPT_ENCODING)); + assertEquals("gzip", r.getHeaderString(CONTENT_ENCODING)); + } + + @Test + public void testContentEncodingSkippedForNoEntity() { + Client client = ClientBuilder.newClient(new ClientConfig( + EncodingFilter.class, + GZipEncoder.class, + DeflateEncoder.class + ).property(ClientProperties.USE_ENCODING, "gzip").connectorProvider(new TestConnector())); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.get(); + assertEquals("deflate,gzip,x-gzip", r.getHeaderString(ACCEPT_ENCODING)); + assertNull(r.getHeaderString(CONTENT_ENCODING)); + } + + @Test + public void testUnsupportedContentEncoding() { + Client client = ClientBuilder.newClient(new ClientConfig( + EncodingFilter.class, + GZipEncoder.class, + DeflateEncoder.class + ).property(ClientProperties.USE_ENCODING, "non-gzip").connectorProvider(new TestConnector())); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.get(); + assertEquals("deflate,gzip,x-gzip", r.getHeaderString(ACCEPT_ENCODING)); + assertNull(r.getHeaderString(CONTENT_ENCODING)); + } + + /** + * Reproducer for JERSEY-2028. + * + * @see #testClosingClientResponseStreamRetrievedByValueOnError + */ + @Test + public void testClosingClientResponseStreamRetrievedByResponseOnError() { + final TestInputStream responseStream = new TestInputStream(); + + Client client = ClientBuilder.newClient(new ClientConfig() + .connectorProvider(new TestConnector() { + @Override + public ClientResponse apply(ClientRequest requestContext) throws ProcessingException { + final ClientResponse responseContext = new ClientResponse(Response.Status.OK, requestContext); + responseContext.header(CONTENT_ENCODING, "gzip"); + responseContext.setEntityStream(responseStream); + return responseContext; + } + }) + .register(new EncodingFeature(GZipEncoder.class, DeflateEncoder.class))); + + final Response response = client.target(UriBuilder.fromUri("/").build()).request().get(); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals("gzip", response.getHeaderString(CONTENT_ENCODING)); + + try { + response.readEntity(String.class); + fail("Exception caused by invalid gzip stream expected."); + } catch (ProcessingException ex) { + assertTrue("Response input stream not closed when exception is thrown.", responseStream.isClosed); + } + } + + /** + * Reproducer for JERSEY-2028. + * + * @see #testClosingClientResponseStreamRetrievedByResponseOnError + */ + @Test + public void testClosingClientResponseStreamRetrievedByValueOnError() { + final TestInputStream responseStream = new TestInputStream(); + + Client client = ClientBuilder.newClient(new ClientConfig() + .connectorProvider(new TestConnector() { + @Override + public ClientResponse apply(ClientRequest requestContext) throws ProcessingException { + final ClientResponse responseContext = new ClientResponse(Response.Status.OK, requestContext); + responseContext.header(CONTENT_ENCODING, "gzip"); + responseContext.setEntityStream(responseStream); + return responseContext; + } + }) + .register(new EncodingFeature(GZipEncoder.class, DeflateEncoder.class))); + + try { + client.target(UriBuilder.fromUri("/").build()).request().get(String.class); + fail("Exception caused by invalid gzip stream expected."); + } catch (ProcessingException ex) { + assertTrue("Response input stream not closed when exception is thrown.", responseStream.isClosed); + } + } + + private static class TestConnector implements Connector, ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return this; + } + + @Override + public ClientResponse apply(ClientRequest requestContext) { + final ClientResponse responseContext = new ClientResponse( + Response.Status.OK, requestContext); + + String headerValue = requestContext.getHeaderString(ACCEPT_ENCODING); + if (headerValue != null) { + responseContext.header(ACCEPT_ENCODING, headerValue); + } + headerValue = requestContext.getHeaderString(CONTENT_ENCODING); + if (headerValue != null) { + responseContext.header(CONTENT_ENCODING, headerValue); + } + return responseContext; + } + + @Override + public Future<?> apply(ClientRequest clientRequest, AsyncConnectorCallback callback) { + throw new UnsupportedOperationException("Asynchronous execution not supported."); + } + + @Override + public void close() { + // do nothing + } + + @Override + public String getName() { + return "test-connector"; + } + } + + private static class TestInputStream extends ByteArrayInputStream { + + private boolean isClosed; + + private TestInputStream() { + super("test".getBytes()); + } + + @Override + public void close() throws IOException { + isClosed = true; + super.close(); + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/filter/HttpBasicAuthFilterTest.java b/core-client/src/test/java/org/glassfish/jersey/client/filter/HttpBasicAuthFilterTest.java new file mode 100644 index 0000000..43f2511 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/filter/HttpBasicAuthFilterTest.java
@@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.filter; + + +import java.util.concurrent.Future; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.internal.util.Base64; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * HTTP Basic authentication filter test. + * + * @author Martin Matula + */ +public class HttpBasicAuthFilterTest { + + @Test + public void testGet() { + Client client = ClientBuilder.newClient(new ClientConfig(HttpAuthenticationFeature.basic("Uzivatelske jmeno", "Heslo")) + .connectorProvider(new TestConnector())); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.get(); + + assertEquals("Basic " + Base64.encodeAsString("Uzivatelske jmeno:Heslo"), r.getHeaderString(HttpHeaders.AUTHORIZATION)); + } + + @Test + public void testBlankUsernamePassword() { + Client client = ClientBuilder.newClient(new ClientConfig(HttpAuthenticationFeature.basic("", "")) + .connectorProvider(new TestConnector())); + Invocation.Builder invBuilder = client.target(UriBuilder.fromUri("/").build()).request(); + Response r = invBuilder.get(); + + assertEquals("Basic " + Base64.encodeAsString(":"), r.getHeaderString(HttpHeaders.AUTHORIZATION)); + } + + private static class TestConnector implements Connector, ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return this; + } + + @Override + public ClientResponse apply(ClientRequest requestContext) { + final ClientResponse responseContext = new ClientResponse( + Response.Status.OK, requestContext); + + final String headerValue = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); + if (headerValue != null) { + responseContext.header(HttpHeaders.AUTHORIZATION, headerValue); + } + return responseContext; + } + + @Override + public Future<?> apply(ClientRequest clientRequest, AsyncConnectorCallback callback) { + throw new UnsupportedOperationException("Asynchronous execution not supported."); + } + + @Override + public void close() { + // do nothing + } + + @Override + public String getName() { + return null; + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/spi/CachingConnectorProviderTest.java b/core-client/src/test/java/org/glassfish/jersey/client/spi/CachingConnectorProviderTest.java new file mode 100644 index 0000000..c3ea160 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/spi/CachingConnectorProviderTest.java
@@ -0,0 +1,110 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.spi; + +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Caching connector provider unit tests. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class CachingConnectorProviderTest { + public static class ReferenceCountingNullConnector implements Connector, ConnectorProvider { + + private static final AtomicInteger counter = new AtomicInteger(0); + + @Override + public ClientResponse apply(ClientRequest request) { + throw new ProcessingException("test"); + } + + @Override + public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) { + throw new ProcessingException("test-async"); + } + + @Override + public void close() { + // do nothing + } + + @Override + public String getName() { + return null; + } + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + counter.incrementAndGet(); + return this; + } + + public int getCount() { + return counter.get(); + } + } + + @Test + public void testCachingConnector() { + final ReferenceCountingNullConnector connectorProvider = new ReferenceCountingNullConnector(); + final CachingConnectorProvider cachingConnectorProvider = new CachingConnectorProvider(connectorProvider); + final ClientConfig configuration = new ClientConfig().connectorProvider(cachingConnectorProvider).getConfiguration(); + + Client client1 = ClientBuilder.newClient(configuration); + try { + client1.target(UriBuilder.fromUri("/").build()).request().get(); + } catch (ProcessingException ce) { + assertEquals("test", ce.getMessage()); + assertEquals(1, connectorProvider.getCount()); + } + try { + client1.target(UriBuilder.fromUri("/").build()).request().async().get(); + } catch (ProcessingException ce) { + assertEquals("test-async", ce.getMessage()); + assertEquals(1, connectorProvider.getCount()); + } + + Client client2 = ClientBuilder.newClient(configuration); + try { + client2.target(UriBuilder.fromUri("/").build()).request().get(); + } catch (ProcessingException ce) { + assertEquals("test", ce.getMessage()); + assertEquals(1, connectorProvider.getCount()); + } + try { + client2.target(UriBuilder.fromUri("/").build()).request().async().get(); + } catch (ProcessingException ce) { + assertEquals("test-async", ce.getMessage()); + assertEquals(1, connectorProvider.getCount()); + } + } +}
diff --git a/core-client/src/test/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/core-client/src/test/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable new file mode 100644 index 0000000..8d394f3 --- /dev/null +++ b/core-client/src/test/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable
@@ -0,0 +1,2 @@ +org.glassfish.jersey.client.AutoDiscoverableClientTest$CommonAutoDiscoverable +org.glassfish.jersey.client.AutoDiscoverableClientTest$LifecycleListenerAutoDiscoverable \ No newline at end of file
diff --git a/core-common/pom.xml b/core-common/pom.xml new file mode 100644 index 0000000..37de72a --- /dev/null +++ b/core-common/pom.xml
@@ -0,0 +1,252 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.glassfish.jersey</groupId> + <artifactId>project</artifactId> + <version>2.28-SNAPSHOT</version> + </parent> + + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-common</artifactId> + <packaging>jar</packaging> + <name>jersey-core-common</name> + + <description>Jersey core common packages</description> + + <licenses> + <license> + <name>EPL 2.0</name> + <url>http://www.eclipse.org/legal/epl-2.0</url> + <distribution>repo</distribution> + <comments>Except for Guava, JSR-166 files, Dropwizard Monitoring inspired classes, ASM and Jackson JAX-RS Providers. + See also https://github.com/eclipse-ee4j/jersey/blob/master/NOTICE.md</comments> + </license> + <license> + <name>The GNU General Public License (GPL), Version 2, With Classpath Exception</name> + <url>https://www.gnu.org/software/classpath/license.html</url> + <distribution>repo</distribution> + <comments>Except for Guava, and JSR-166 files. + See also https://github.com/eclipse-ee4j/jersey/blob/master/NOTICE.md</comments> + </license> + <license> + <name>Apache License, 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.html</url> + <distribution>repo</distribution> + <comments>Google Guava @ org.glassfish.jersey.internal.guava</comments> + </license> + <license> + <name>Public Domain</name> + <url>https://creativecommons.org/publicdomain/zero/1.0/</url> + <distribution>repo</distribution> + <comments>JSR-166 Extension to JEP 266 @ org.glassfish.jersey.internal.jsr166</comments> + </license> + </licenses> + + <build> + <resources> + <resource> + <directory>${basedir}/src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> + + <testResources> + <testResource> + <directory>${basedir}/src/test/resources</directory> + <filtering>true</filtering> + </testResource> + </testResources> + + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <inherited>false</inherited> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + <compilerArguments> + <!-- Do not warn about using sun.misc.Unsafe --> + <XDignore.symbol.file /> + </compilerArguments> + <showWarnings>false</showWarnings> + <fork>false</fork> + </configuration> + </plugin> + <plugin> + <groupId>com.sun.istack</groupId> + <artifactId>maven-istack-commons-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <inherited>true</inherited> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + <extensions>true</extensions> + <configuration> + <instructions> + <!-- Note: When you're changing these properties change them also in bundles/jaxrs-ri/pom.xml. --> + <Import-Package> + sun.misc.*;resolution:=optional, + * + </Import-Package> + <Export-Package>org.glassfish.jersey.*;version=${project.version}</Export-Package> + <Private-Package>org.glassfish.jersey.osgi</Private-Package> + </instructions> + <unpackBundle>true</unpackBundle> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>buildnumber-maven-plugin</artifactId> + <configuration> + <format>{0,date,yyyy-MM-dd HH:mm:ss}</format> + <items> + <item>timestamp</item> + </items> + </configuration> + <executions> + <execution> + <phase>validate</phase> + <goals> + <goal>create</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- Execute test classes in parallel - 1 thread per CPU core. --> + <parallel>classesAndMethods</parallel> + <perCoreThreadCount>true</perCoreThreadCount> + <threadCount>1</threadCount> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + </dependency> + <dependency> + <groupId>javax.annotation</groupId> + <artifactId>javax.annotation-api</artifactId> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.glassfish.hk2.external</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + <dependency> + <groupId>org.glassfish.hk2</groupId> + <artifactId>osgi-resource-locator</artifactId> + </dependency> + + <dependency> + <!-- Must be declared before JUnit dependency, otherwise not visible to JUnit. --> + <groupId>org.jmockit</groupId> + <artifactId>jmockit</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <profiles> + <profile> + <id>securityOff</id> + <properties> + <surefire.security.argline /> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <excludes> + <exclude>**/SecurityManagerConfiguredTest.java</exclude> + <exclude>**/ReflectionHelperTest.java</exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>sonar</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- disable parallel execution so that JaCoCo listener can properly work --> + <parallel>none</parallel> + <perCoreThreadCount>false</perCoreThreadCount> + </configuration> + </plugin> + </plugins> + </build> + </profile> + + </profiles> + + <properties> + <surefire.security.argline>-Djava.security.manager -Djava.security.policy=${project.build.directory}/test-classes/surefire.policy</surefire.security.argline> + </properties> + +</project>
diff --git a/core-common/src/main/java/org/glassfish/jersey/Beta.java b/core-common/src/main/java/org/glassfish/jersey/Beta.java new file mode 100644 index 0000000..b855978 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/Beta.java
@@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; + +/** + * Marker of a public Jersey API that is still in "beta" non-final version. + * <p> + * This annotation signals that the annotated public Jersey API (package, class, method or field) + * has not been fully stabilized yet. As such, the API is subject to backward-incompatible changes + * (or even removal) in a future Jersey release. Jersey development team does not make any guarantees + * to retain backward compatibility of a {@code @Beta}-annotated Jersey API. + * </p> + * <p> + * This annotation does not indicate inferior quality or performance of the API, just informs that the + * API may still evolve in the future in a backward-incompatible ways. Jersey users may use beta APIs + * in their applications keeping in mind potential cost of extra work associated with an upgrade + * to a newer Jersey version. + * </p> + * <p> + * Once a {@code @Beta}-annotated Jersey API reaches the desired maturity, the {@code @Beta} annotation + * will be removed from such API and the API will become part of a stable public Jersey API. + * </p> + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@Retention(RetentionPolicy.CLASS) +@Documented +@Target({ANNOTATION_TYPE, TYPE, CONSTRUCTOR, METHOD, FIELD, PACKAGE}) +public @interface Beta { +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java b/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java new file mode 100644 index 0000000..3218d3b --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java
@@ -0,0 +1,336 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.RuntimeType; + +import org.glassfish.jersey.internal.util.PropertiesClass; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +/** + * Common (server/client) Jersey configuration properties. + * + * @author Michal Gajdos + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +@PropertiesClass +public final class CommonProperties { + + private static final Map<String, String> LEGACY_FALLBACK_MAP = new HashMap<String, String>(); + + static { + LEGACY_FALLBACK_MAP.put(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_CLIENT, + "jersey.config.contentLength.buffer.client"); + LEGACY_FALLBACK_MAP.put(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, + "jersey.config.contentLength.buffer.server"); + LEGACY_FALLBACK_MAP.put(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE_CLIENT, + "jersey.config.disableAutoDiscovery.client"); + LEGACY_FALLBACK_MAP.put(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE_SERVER, + "jersey.config.disableAutoDiscovery.server"); + LEGACY_FALLBACK_MAP.put(CommonProperties.JSON_PROCESSING_FEATURE_DISABLE_CLIENT, + "jersey.config.disableJsonProcessing.client"); + LEGACY_FALLBACK_MAP.put(CommonProperties.JSON_PROCESSING_FEATURE_DISABLE_SERVER, + "jersey.config.disableJsonProcessing.server"); + LEGACY_FALLBACK_MAP.put(CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE_CLIENT, + "jersey.config.disableMetainfServicesLookup.client"); + LEGACY_FALLBACK_MAP.put(CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE_SERVER, + "jersey.config.disableMetainfServicesLookup.server"); + LEGACY_FALLBACK_MAP.put(CommonProperties.MOXY_JSON_FEATURE_DISABLE_CLIENT, + "jersey.config.disableMoxyJson.client"); + LEGACY_FALLBACK_MAP.put(CommonProperties.MOXY_JSON_FEATURE_DISABLE_SERVER, + "jersey.config.disableMoxyJson.server"); + } + + /** + * If {@code true} then disable feature auto discovery globally on client/server. + * <p> + * By default auto discovery is automatically enabled. The value of this property may be overridden by the client/server + * variant of this property. + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String FEATURE_AUTO_DISCOVERY_DISABLE = "jersey.config.disableAutoDiscovery"; + + /** + * Client-specific version of {@link CommonProperties#FEATURE_AUTO_DISCOVERY_DISABLE}. + * + * If present, it overrides the generic one for the client environment. + * @since 2.8 + */ + public static final String FEATURE_AUTO_DISCOVERY_DISABLE_CLIENT = "jersey.config.client.disableAutoDiscovery"; + + /** + * Server-specific version of {@link CommonProperties#FEATURE_AUTO_DISCOVERY_DISABLE}. + * + * If present, it overrides the generic one for the server environment. + * @since 2.8 + */ + public static final String FEATURE_AUTO_DISCOVERY_DISABLE_SERVER = "jersey.config.server.disableAutoDiscovery"; + + /** + * If {@code true} then disable configuration of Json Processing (JSR-353) feature. + * <p> + * By default Json Processing is automatically enabled. The value of this property may be overridden by the client/server + * variant of this property. + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String JSON_PROCESSING_FEATURE_DISABLE = "jersey.config.disableJsonProcessing"; + + /** + * Client-specific version of {@link CommonProperties#JSON_PROCESSING_FEATURE_DISABLE}. + * + * If present, it overrides the generic one for the client environment. + * @since 2.8 + */ + public static final String JSON_PROCESSING_FEATURE_DISABLE_CLIENT = "jersey.config.client.disableJsonProcessing"; + + /** + * Server-specific version of {@link CommonProperties#JSON_PROCESSING_FEATURE_DISABLE}. + * + * If present, it overrides the generic one for the server environment. + * @since 2.8 + */ + public static final String JSON_PROCESSING_FEATURE_DISABLE_SERVER = "jersey.config.server.disableJsonProcessing"; + + /** + * If {@code true} then disable META-INF/services lookup globally on client/server. + * <p> + * By default Jersey looks up SPI implementations described by META-INF/services/* files. + * Then you can register appropriate provider classes by {@link javax.ws.rs.core.Application}. + * </p> + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + * + * @since 2.1 + */ + public static final String METAINF_SERVICES_LOOKUP_DISABLE = "jersey.config.disableMetainfServicesLookup"; + + /** + * Client-specific version of {@link CommonProperties#METAINF_SERVICES_LOOKUP_DISABLE}. + * + * If present, it overrides the generic one for the client environment. + * @since 2.8 + */ + public static final String METAINF_SERVICES_LOOKUP_DISABLE_CLIENT = "jersey.config.client.disableMetainfServicesLookup"; + + /** + * Server-specific version of {@link CommonProperties#METAINF_SERVICES_LOOKUP_DISABLE}. + * + * If present, it overrides the generic one for the server environment. + * @since 2.8 + */ + public static final String METAINF_SERVICES_LOOKUP_DISABLE_SERVER = "jersey.config.server.disableMetainfServicesLookup"; + + /** + * If {@code true} then disable configuration of MOXy Json feature. + * <p> + * By default MOXy Json is automatically enabled. The value of this property may be overridden by the client/server + * variant of this property. + * <p> + * The default value is {@code false}. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String MOXY_JSON_FEATURE_DISABLE = "jersey.config.disableMoxyJson"; + + /** + * Client-specific version of {@link CommonProperties#MOXY_JSON_FEATURE_DISABLE}. + * + * If present, it overrides the generic one for the client environment. + * @since 2.8 + */ + public static final String MOXY_JSON_FEATURE_DISABLE_CLIENT = "jersey.config.client.disableMoxyJson"; + + /** + * Server-specific version of {@link CommonProperties#MOXY_JSON_FEATURE_DISABLE}. + * + * If present, it overrides the generic one for the server environment. + * @since 2.8 + */ + public static final String MOXY_JSON_FEATURE_DISABLE_SERVER = "jersey.config.server.disableMoxyJson"; + + /** + * An integer value that defines the buffer size used to buffer the outbound message entity in order to + * determine its size and set the value of HTTP <tt>{@value javax.ws.rs.core.HttpHeaders#CONTENT_LENGTH}</tt> header. + * <p> + * If the entity size exceeds the configured buffer size, the buffering would be cancelled and the entity size + * would not be determined. Value less or equal to zero disable the buffering of the entity at all. + * </p> + * The value of this property may be overridden by the client/server variant of this property by defining the suffix + * to this property "<tt>.server</tt>" or "<tt>.client</tt>" + * (<tt>{@value}.server</tt> or <tt>{@value}.client</tt>). + * <p> + * The default value is <tt>8192</tt>. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String OUTBOUND_CONTENT_LENGTH_BUFFER = "jersey.config.contentLength.buffer"; + + /** + * Client-specific version of {@link CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER}. + * + * If present, it overrides the generic one for the client environment. + * @since 2.8 + */ + public static final String OUTBOUND_CONTENT_LENGTH_BUFFER_CLIENT = "jersey.config.client.contentLength.buffer"; + + /** + * Server-specific version of {@link CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER}. + * + * If present, it overrides the generic one for the server environment. + * @since 2.8 + */ + public static final String OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER = "jersey.config.server.contentLength.buffer"; + + /** + * Prevent instantiation. + */ + private CommonProperties() { + } + + /** + * Get the value of the specified property. + * + * If the property is not set or the actual property value type is not compatible with the specified type, the method will + * return {@code null}. + * + * @param properties Map of properties to get the property value from. + * @param propertyName Name of the property. + * @param type Type to retrieve the value as. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static Object getValue(final Map<String, ?> properties, final String propertyName, final Class<?> type) { + return PropertiesHelper.getValue(properties, propertyName, type, CommonProperties.LEGACY_FALLBACK_MAP); + } + + /** + * Get the value of the specified property. + * + * If the property is not set or the real value type is not compatible with {@code defaultValue} type, + * the specified {@code defaultValue} is returned. Calling this method is equivalent to calling + * {@code CommonProperties.getValue(properties, key, defaultValue, (Class<T>) defaultValue.getClass())} + * + * @param properties Map of properties to get the property value from. + * @param propertyName Name of the property. + * @param defaultValue Default value if property is not registered + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, final String propertyName, final T defaultValue) { + return PropertiesHelper.getValue(properties, propertyName, defaultValue, CommonProperties.LEGACY_FALLBACK_MAP); + } + + /** + * Get the value of the specified property. + * + * If the property is not set or the real value type is not compatible with {@code defaultValue} type, + * the specified {@code defaultValue} is returned. Calling this method is equivalent to calling + * {@code CommonProperties.getValue(properties, runtimeType, key, defaultValue, (Class<T>) defaultValue.getClass())} + * + * @param properties Map of properties to get the property value from. + * @param runtime Runtime type which is used to check whether there is a property with the same + * {@code key} but post-fixed by runtime type (<tt>.server</tt> + * or {@code .client}) which would override the {@code key} property. + * @param propertyName Name of the property. + * @param defaultValue Default value if property is not registered + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, + final RuntimeType runtime, + final String propertyName, + final T defaultValue) { + return PropertiesHelper.getValue(properties, runtime, propertyName, defaultValue, CommonProperties.LEGACY_FALLBACK_MAP); + } + + /** + * Get the value of the specified property. + * + * If the property is not set or the real value type is not compatible with the specified value type, + * returns {@code defaultValue}. + * + * @param properties Map of properties to get the property value from. + * @param runtime Runtime type which is used to check whether there is a property with the same + * {@code key} but post-fixed by runtime type (<tt>.server</tt> + * or {@code .client}) which would override the {@code key} property. + * @param propertyName Name of the property. + * @param defaultValue Default value if property is not registered + * @param type Type to retrieve the value as. + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, + final RuntimeType runtime, + final String propertyName, + final T defaultValue, + final Class<T> type) { + return PropertiesHelper.getValue(properties, runtime, propertyName, defaultValue, type, + CommonProperties.LEGACY_FALLBACK_MAP); + } + + /** + * Get the value of the specified property. + * + * If the property is not set or the actual property value type is not compatible with the specified type, the method will + * return {@code null}. + * + * @param properties Map of properties to get the property value from. + * @param runtime Runtime type which is used to check whether there is a property with the same + * {@code key} but post-fixed by runtime type (<tt>.server</tt> + * or {@code .client}) which would override the {@code key} property. + * @param propertyName Name of the property. + * @param type Type to retrieve the value as. + * @param <T> Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static <T> T getValue(final Map<String, ?> properties, + final RuntimeType runtime, + final String propertyName, + final Class<T> type) { + return PropertiesHelper.getValue(properties, runtime, propertyName, type, CommonProperties.LEGACY_FALLBACK_MAP); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/ExtendedConfig.java b/core-common/src/main/java/org/glassfish/jersey/ExtendedConfig.java new file mode 100644 index 0000000..ca0c24f --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/ExtendedConfig.java
@@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +import javax.ws.rs.core.Configuration; + +/** + * Extended common runtime configuration. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public interface ExtendedConfig extends Configuration { + + /** + * Get the value of the property with a given name converted to {@code boolean}. + * Returns {@code false} if the value is not convertible. + * + * @param name property name. + * @return {@code boolean} property value or {@code false} if the property is not + * convertible. + */ + public boolean isProperty(final String name); +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/InjectionManagerProvider.java b/core-common/src/main/java/org/glassfish/jersey/InjectionManagerProvider.java new file mode 100644 index 0000000..5bce4c4 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/InjectionManagerProvider.java
@@ -0,0 +1,152 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.ReaderInterceptorContext; +import javax.ws.rs.ext.WriterInterceptorContext; + +import org.glassfish.jersey.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManagerSupplier; + +/** + * Utility class with static methods that extract {@link InjectionManager injection manager} + * from various JAX-RS components. This class can be used when no injection is possible by + * {@link javax.ws.rs.core.Context} or {@link javax.inject.Inject} annotation due to character of + * provider but there is a need to get any service from {@link InjectionManager}. + * <p> + * Injections are not possible for example when a provider is registered as an instance on the client. + * In this case the runtime will not inject the instance as this instance might be used in other client + * runtimes too. + * </p> + * <p> + * Example. This is the class using a standard injection: + * <pre> + * public static class MyWriterInterceptor implements WriterInterceptor { + * @Inject + * MyInjectedService service; + * + * @Override + * public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + * Something something = service.getSomething(); + * ... + * } + * + * } + * </pre> + * </p> + * <p> + * If this injection is not possible then this construct can be used: + * <pre> + * public static class MyWriterInterceptor implements WriterInterceptor { + * + * @Override + * public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + * InjectionManager injectionManager = InjectionManagerProvider.getInjectionManager(context); + * MyInjectedService service = injectionManager.getInstance(MyInjectedService.class); + * Something something = service.getSomething(); + * ... + * } + * } + * </pre> + * </p> + * <p> + * Note, that this injection support is intended mostly for injection of custom user types. JAX-RS types + * are usually available without need for injections in method parameters. However, when injection of custom + * type is needed it is preferred to use standard injections if it is possible rather than injection support + * provided by this class. + * </p> + * <p> + * User returned {@code InjectionManager} only for purpose of getting services (do not change the state of the injection manager). + * </p> + * + * + * @author Miroslav Fuksa + * + * @since 2.6 + */ +public class InjectionManagerProvider { + + /** + * Extract and return injection manager from {@link javax.ws.rs.ext.WriterInterceptorContext writerInterceptorContext}. + * The method can be used to inject custom types into a {@link javax.ws.rs.ext.WriterInterceptor}. + * + * @param writerInterceptorContext Writer interceptor context. + * + * @return injection manager. + * + * @throws java.lang.IllegalArgumentException when {@code writerInterceptorContext} is not a default + * Jersey implementation provided by Jersey as argument in the + * {@link javax.ws.rs.ext.WriterInterceptor#aroundWriteTo(javax.ws.rs.ext.WriterInterceptorContext)} method. + */ + public static InjectionManager getInjectionManager(WriterInterceptorContext writerInterceptorContext) { + if (!(writerInterceptorContext instanceof InjectionManagerSupplier)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_SERVICE_LOCATOR_PROVIDER_INSTANCE_FEATURE_WRITER_INTERCEPTOR_CONTEXT( + writerInterceptorContext.getClass().getName())); + } + return ((InjectionManagerSupplier) writerInterceptorContext).getInjectionManager(); + } + + /** + * Extract and return injection manager from {@link javax.ws.rs.ext.ReaderInterceptorContext readerInterceptorContext}. + * The method can be used to inject custom types into a {@link javax.ws.rs.ext.ReaderInterceptor}. + * + * @param readerInterceptorContext Reader interceptor context. + * + * @return injection manager. + * + * @throws java.lang.IllegalArgumentException when {@code readerInterceptorContext} is not a default + * Jersey implementation provided by Jersey as argument in the + * {@link javax.ws.rs.ext.ReaderInterceptor#aroundReadFrom(javax.ws.rs.ext.ReaderInterceptorContext)} method. + + */ + public static InjectionManager getInjectionManager(ReaderInterceptorContext readerInterceptorContext) { + if (!(readerInterceptorContext instanceof InjectionManagerSupplier)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_SERVICE_LOCATOR_PROVIDER_INSTANCE_FEATURE_READER_INTERCEPTOR_CONTEXT( + readerInterceptorContext.getClass().getName())); + } + return ((InjectionManagerSupplier) readerInterceptorContext).getInjectionManager(); + } + + /** + * Extract and return injection manager from {@link javax.ws.rs.core.FeatureContext featureContext}. + * The method can be used to inject custom types into a {@link javax.ws.rs.core.Feature}. + * <p> + * Note that features are utilized during initialization phase when not all providers are registered yet. + * It is undefined which injections are already available in this phase. + * </p> + * + * @param featureContext Feature context. + * + * @return injection manager. + * + * @throws java.lang.IllegalArgumentException when {@code writerInterceptorContext} is not a default + * Jersey instance provided by Jersey + * in {@link javax.ws.rs.core.Feature#configure(javax.ws.rs.core.FeatureContext)} method. + */ + public static InjectionManager getInjectionManager(FeatureContext featureContext) { + if (!(featureContext instanceof InjectionManagerSupplier)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_SERVICE_LOCATOR_PROVIDER_INSTANCE_FEATURE_CONTEXT( + featureContext.getClass().getName())); + } + return ((InjectionManagerSupplier) featureContext).getInjectionManager(); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/JerseyPriorities.java b/core-common/src/main/java/org/glassfish/jersey/JerseyPriorities.java new file mode 100644 index 0000000..a35a764 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/JerseyPriorities.java
@@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +import javax.ws.rs.Priorities; + +/** + * Built-in Jersey-specific priority constants to be used along with {@link javax.ws.rs.Priorities} where finer-grained + * categorization is required. + * + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +public class JerseyPriorities { + + private JerseyPriorities() { + // prevents instantiation + } + + /** + * Priority for components that have to be called AFTER message encoders/decoders filters/interceptors. + * The constant has to be higher than {@link javax.ws.rs.Priorities#ENTITY_CODER} in order to force the + * processing after the components with {@code Priorities.ENTITY_CODER} are processed. + */ + public static final int POST_ENTITY_CODER = Priorities.ENTITY_CODER + 100; +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/Severity.java b/core-common/src/main/java/org/glassfish/jersey/Severity.java new file mode 100644 index 0000000..2b42c13 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/Severity.java
@@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +/** + * Common severity. + */ +public enum Severity { + /** + * Fatal. + */ + FATAL, + /** + * Warning. + */ + WARNING, + /** + * Hint. + */ + HINT +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java new file mode 100644 index 0000000..0cca232 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java
@@ -0,0 +1,919 @@ +/* + * Copyright (c) 2007, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.AccessController; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import org.glassfish.jersey.internal.LocalizationMessages; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +/** + * Utility class, which helps to configure {@link SSLContext} instances. + * + * For example: + * <pre> + * SslConfigurator sslConfig = SslConfigurator.newInstance() + * .trustStoreFile("truststore.jks") + * .trustStorePassword("asdfgh") + * .trustStoreType("JKS") + * .trustManagerFactoryAlgorithm("PKIX") + * + * .keyStoreFile("keystore.jks") + * .keyPassword("asdfgh") + * .keyStoreType("JKS") + * .keyManagerFactoryAlgorithm("SunX509") + * .keyStoreProvider("SunJSSE") + * + * .securityProtocol("SSL"); + * + * SSLContext sslContext = sslConfig.createSSLContext(); + * </pre> + * + * @author Alexey Stashok + * @author Hubert Iwaniuk + * @author Bruno Harbulot + * @author Marek Potociar (marek.potociar at oracle.com) + */ +@SuppressWarnings("UnusedDeclaration") +public final class SslConfigurator { + + /** + * <em>Trust</em> store provider name. + * + * The value MUST be a {@code String} representing the name of a <em>trust</em> store provider. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String TRUST_STORE_PROVIDER = "javax.net.ssl.trustStoreProvider"; + /** + * <em>Key</em> store provider name. + * + * The value MUST be a {@code String} representing the name of a <em>trust</em> store provider. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String KEY_STORE_PROVIDER = "javax.net.ssl.keyStoreProvider"; + /** + * <em>Trust</em> store file name. + * + * The value MUST be a {@code String} representing the name of a <em>trust</em> store file. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String TRUST_STORE_FILE = "javax.net.ssl.trustStore"; + /** + * <em>Key</em> store file name. + * + * The value MUST be a {@code String} representing the name of a <em>key</em> store file. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String KEY_STORE_FILE = "javax.net.ssl.keyStore"; + /** + * <em>Trust</em> store file password - the password used to unlock the <em>trust</em> store file. + * + * The value MUST be a {@code String} representing the <em>trust</em> store file password. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword"; + /** + * <em>Key</em> store file password - the password used to unlock the <em>trust</em> store file. + * + * The value MUST be a {@code String} representing the <em>key</em> store file password. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword"; + /** + * <em>Trust</em> store type (see {@link java.security.KeyStore#getType()} for more info). + * + * The value MUST be a {@code String} representing the <em>trust</em> store type name. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType"; + /** + * <em>Key</em> store type (see {@link java.security.KeyStore#getType()} for more info). + * + * The value MUST be a {@code String} representing the <em>key</em> store type name. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String KEY_STORE_TYPE = "javax.net.ssl.keyStoreType"; + /** + * <em>Key</em> manager factory algorithm name. + * + * The value MUST be a {@code String} representing the <em>key</em> manager factory algorithm name. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String KEY_MANAGER_FACTORY_ALGORITHM = "ssl.keyManagerFactory.algorithm"; + /** + * <em>Key</em> manager factory provider name. + * + * The value MUST be a {@code String} representing the <em>key</em> manager factory provider name. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String KEY_MANAGER_FACTORY_PROVIDER = "ssl.keyManagerFactory.provider"; + /** + * <em>Trust</em> manager factory algorithm name. + * + * The value MUST be a {@code String} representing the <em>trust</em> manager factory algorithm name. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String TRUST_MANAGER_FACTORY_ALGORITHM = "ssl.trustManagerFactory.algorithm"; + /** + * <em>Trust</em> manager factory provider name. + * + * The value MUST be a {@code String} representing the <em>trust</em> manager factory provider name. + * <p> + * No default value is set. + * </p> + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String TRUST_MANAGER_FACTORY_PROVIDER = "ssl.trustManagerFactory.provider"; + /** + * Default SSL configuration that is used to create default SSL context instances that do not take into + * account system properties. + */ + private static final SslConfigurator DEFAULT_CONFIG_NO_PROPS = new SslConfigurator(false); + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SslConfigurator.class.getName()); + + private KeyStore keyStore; + private KeyStore trustStore; + + private String trustStoreProvider; + private String keyStoreProvider; + + private String trustStoreType; + private String keyStoreType; + + private char[] trustStorePass; + private char[] keyStorePass; + private char[] keyPass; + + private String trustStoreFile; + private String keyStoreFile; + + private byte[] trustStoreBytes; + private byte[] keyStoreBytes; + + private String trustManagerFactoryAlgorithm; + private String keyManagerFactoryAlgorithm; + + private String trustManagerFactoryProvider; + private String keyManagerFactoryProvider; + + private String securityProtocol = "TLS"; + + /** + * Get a new instance of a {@link SSLContext} configured using default configuration settings. + * + * The default SSL configuration is initialized from system properties. This method is a shortcut + * for {@link #getDefaultContext(boolean) getDefaultContext(true)}. + * + * @return new instance of a default SSL context initialized from system properties. + */ + public static SSLContext getDefaultContext() { + return getDefaultContext(true); + } + + /** + * Get a new instance of a {@link SSLContext} configured using default configuration settings. + * + * If {@code readSystemProperties} parameter is set to {@code true}, the default SSL configuration + * is initialized from system properties. + * + * @param readSystemProperties if {@code true}, the default SSL context will be initialized using + * system properties. + * @return new instance of a default SSL context initialized from system properties. + */ + public static SSLContext getDefaultContext(boolean readSystemProperties) { + if (readSystemProperties) { + return new SslConfigurator(true).createSSLContext(); + } else { + return DEFAULT_CONFIG_NO_PROPS.createSSLContext(); + } + } + + /** + * Get a new & initialized SSL configurator instance. + * + * The instance {@link #retrieve(java.util.Properties) retrieves} the initial configuration from + * {@link System#getProperties() system properties}. + * + * @return new & initialized SSL configurator instance. + */ + public static SslConfigurator newInstance() { + return new SslConfigurator(false); + } + + /** + * Get a new SSL configurator instance. + * + * @param readSystemProperties if {@code true}, {@link #retrieve(java.util.Properties) Retrieves} + * the initial configuration from {@link System#getProperties()}, + * otherwise the instantiated configurator will be empty. + * @return new SSL configurator instance. + */ + public static SslConfigurator newInstance(boolean readSystemProperties) { + return new SslConfigurator(readSystemProperties); + } + + private SslConfigurator(boolean readSystemProperties) { + if (readSystemProperties) { + retrieve(AccessController.doPrivileged(PropertiesHelper.getSystemProperties())); + } + } + + private SslConfigurator(SslConfigurator that) { + this.keyStore = that.keyStore; + this.trustStore = that.trustStore; + this.trustStoreProvider = that.trustStoreProvider; + this.keyStoreProvider = that.keyStoreProvider; + this.trustStoreType = that.trustStoreType; + this.keyStoreType = that.keyStoreType; + this.trustStorePass = that.trustStorePass; + this.keyStorePass = that.keyStorePass; + this.keyPass = that.keyPass; + this.trustStoreFile = that.trustStoreFile; + this.keyStoreFile = that.keyStoreFile; + this.trustStoreBytes = that.trustStoreBytes; + this.keyStoreBytes = that.keyStoreBytes; + this.trustManagerFactoryAlgorithm = that.trustManagerFactoryAlgorithm; + this.keyManagerFactoryAlgorithm = that.keyManagerFactoryAlgorithm; + this.trustManagerFactoryProvider = that.trustManagerFactoryProvider; + this.keyManagerFactoryProvider = that.keyManagerFactoryProvider; + this.securityProtocol = that.securityProtocol; + } + + /** + * Create a copy of the current SSL configurator instance. + * + * @return copy of the current SSL configurator instance + */ + public SslConfigurator copy() { + return new SslConfigurator(this); + } + + /** + * Set the <em>trust</em> store provider name. + * + * @param trustStoreProvider <em>trust</em> store provider to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustStoreProvider(String trustStoreProvider) { + this.trustStoreProvider = trustStoreProvider; + return this; + } + + /** + * Set the <em>key</em> store provider name. + * + * @param keyStoreProvider <em>key</em> store provider to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStoreProvider(String keyStoreProvider) { + this.keyStoreProvider = keyStoreProvider; + return this; + } + + /** + * Set the type of <em>trust</em> store. + * + * @param trustStoreType type of <em>trust</em> store to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + return this; + } + + /** + * Set the type of <em>key</em> store. + * + * @param keyStoreType type of <em>key</em> store to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + return this; + } + + /** + * Set the password of <em>trust</em> store. + * + * @param password password of <em>trust</em> store to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustStorePassword(String password) { + this.trustStorePass = password.toCharArray(); + return this; + } + + /** + * Set the password of <em>key</em> store. + * + * @param password password of <em>key</em> store to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStorePassword(String password) { + this.keyStorePass = password.toCharArray(); + return this; + } + + /** + * Set the password of <em>key</em> store. + * + * @param password password of <em>key</em> store to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStorePassword(char[] password) { + this.keyStorePass = password.clone(); + return this; + } + + /** + * Set the password of the key in the <em>key</em> store. + * + * @param password password of <em>key</em> to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyPassword(String password) { + this.keyPass = password.toCharArray(); + return this; + } + + /** + * Set the password of the key in the <em>key</em> store. + * + * @param password password of <em>key</em> to set. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyPassword(char[] password) { + this.keyPass = password.clone(); + return this; + } + + /** + * Set the <em>trust</em> store file name. + * <p> + * Setting a trust store instance resets any {@link #trustStore(java.security.KeyStore) trust store instance} + * or {@link #trustStoreBytes(byte[]) trust store payload} value previously set. + * </p> + * + * @param fileName {@link java.io.File file} name of the <em>trust</em> store. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustStoreFile(String fileName) { + this.trustStoreFile = fileName; + this.trustStoreBytes = null; + this.trustStore = null; + return this; + } + + /** + * Set the <em>trust</em> store payload as byte array. + * <p> + * Setting a trust store instance resets any {@link #trustStoreFile(String) trust store file} + * or {@link #trustStore(java.security.KeyStore) trust store instance} value previously set. + * </p> + * + * @param payload <em>trust</em> store payload. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustStoreBytes(byte[] payload) { + this.trustStoreBytes = payload.clone(); + this.trustStoreFile = null; + this.trustStore = null; + return this; + } + + /** + * Set the <em>key</em> store file name. + * <p> + * Setting a key store instance resets any {@link #keyStore(java.security.KeyStore) key store instance} + * or {@link #keyStoreBytes(byte[]) key store payload} value previously set. + * </p> + * + * @param fileName {@link java.io.File file} name of the <em>key</em> store. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStoreFile(String fileName) { + this.keyStoreFile = fileName; + this.keyStoreBytes = null; + this.keyStore = null; + return this; + } + + /** + * Set the <em>key</em> store payload as byte array. + * <p> + * Setting a key store instance resets any {@link #keyStoreFile(String) key store file} + * or {@link #keyStore(java.security.KeyStore) key store instance} value previously set. + * </p> + * + * @param payload <em>key</em> store payload. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStoreBytes(byte[] payload) { + this.keyStoreBytes = payload.clone(); + this.keyStoreFile = null; + this.keyStore = null; + return this; + } + + /** + * Set the <em>trust</em> manager factory algorithm. + * + * @param algorithm the <em>trust</em> manager factory algorithm. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustManagerFactoryAlgorithm(String algorithm) { + this.trustManagerFactoryAlgorithm = algorithm; + return this; + } + + /** + * Set the <em>key</em> manager factory algorithm. + * + * @param algorithm the <em>key</em> manager factory algorithm. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyManagerFactoryAlgorithm(String algorithm) { + this.keyManagerFactoryAlgorithm = algorithm; + return this; + } + + /** + * Set the <em>trust</em> manager factory provider. + * + * @param provider the <em>trust</em> manager factory provider. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustManagerFactoryProvider(String provider) { + this.trustManagerFactoryProvider = provider; + return this; + } + + /** + * Set the <em>key</em> manager factory provider. + * + * @param provider the <em>key</em> manager factory provider. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyManagerFactoryProvider(String provider) { + this.keyManagerFactoryProvider = provider; + return this; + } + + /** + * Set the SSLContext protocol. The default value is {@code TLS} if this is {@code null}. + * + * @param protocol protocol for {@link javax.net.ssl.SSLContext#getProtocol()}. + * @return updated SSL configurator instance. + */ + public SslConfigurator securityProtocol(String protocol) { + this.securityProtocol = protocol; + return this; + } + + /** + * Get the <em>key</em> store instance. + * + * @return <em>key</em> store instance or {@code null} if not explicitly set. + */ + KeyStore getKeyStore() { + return keyStore; + } + + /** + * Set the <em>key</em> store instance. + * <p> + * Setting a key store instance resets any {@link #keyStoreFile(String) key store file} + * or {@link #keyStoreBytes(byte[]) key store payload} value previously set. + * </p> + * + * @param keyStore <em>key</em> store instance. + * @return updated SSL configurator instance. + */ + public SslConfigurator keyStore(KeyStore keyStore) { + this.keyStore = keyStore; + this.keyStoreFile = null; + this.keyStoreBytes = null; + return this; + } + + /** + * Get the <em>trust</em> store instance. + * <p> + * Setting a trust store instance resets any {@link #trustStoreFile(String) trust store file} + * or {@link #trustStoreBytes(byte[]) trust store payload} value previously set. + * </p> + * + * @return <em>trust</em> store instance or {@code null} if not explicitly set. + */ + KeyStore getTrustStore() { + return trustStore; + } + + /** + * Set the <em>trust</em> store instance. + * + * @param trustStore <em>trust</em> store instance. + * @return updated SSL configurator instance. + */ + public SslConfigurator trustStore(KeyStore trustStore) { + this.trustStore = trustStore; + this.trustStoreFile = null; + this.trustStoreBytes = null; + return this; + } + + /** + * Create new SSL context instance using the current SSL context configuration. + * + * @return newly configured SSL context instance. + */ + public SSLContext createSSLContext() { + TrustManagerFactory trustManagerFactory = null; + KeyManagerFactory keyManagerFactory = null; + + KeyStore _keyStore = keyStore; + if (_keyStore == null && (keyStoreBytes != null || keyStoreFile != null)) { + try { + if (keyStoreProvider != null) { + _keyStore = KeyStore.getInstance( + keyStoreType != null ? keyStoreType : KeyStore.getDefaultType(), keyStoreProvider); + } else { + _keyStore = KeyStore.getInstance(keyStoreType != null ? keyStoreType : KeyStore.getDefaultType()); + } + InputStream keyStoreInputStream = null; + try { + if (keyStoreBytes != null) { + keyStoreInputStream = new ByteArrayInputStream(keyStoreBytes); + } else if (!keyStoreFile.equals("NONE")) { + keyStoreInputStream = new FileInputStream(keyStoreFile); + } + _keyStore.load(keyStoreInputStream, keyStorePass); + } finally { + try { + if (keyStoreInputStream != null) { + keyStoreInputStream.close(); + } + } catch (IOException ignored) { + } + } + } catch (KeyStoreException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KS_IMPL_NOT_FOUND(), e); + } catch (CertificateException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KS_CERT_LOAD_ERROR(), e); + } catch (FileNotFoundException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KS_FILE_NOT_FOUND(keyStoreFile), e); + } catch (IOException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KS_LOAD_ERROR(keyStoreFile), e); + } catch (NoSuchProviderException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KS_PROVIDERS_NOT_REGISTERED(), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KS_INTEGRITY_ALGORITHM_NOT_FOUND(), e); + } + } + if (_keyStore != null) { + String kmfAlgorithm = keyManagerFactoryAlgorithm; + if (kmfAlgorithm == null) { + kmfAlgorithm = AccessController.doPrivileged(PropertiesHelper.getSystemProperty( + KEY_MANAGER_FACTORY_ALGORITHM, KeyManagerFactory.getDefaultAlgorithm())); + } + try { + if (keyManagerFactoryProvider != null) { + keyManagerFactory = KeyManagerFactory.getInstance(kmfAlgorithm, keyManagerFactoryProvider); + } else { + keyManagerFactory = KeyManagerFactory.getInstance(kmfAlgorithm); + } + final char[] password = keyPass != null ? keyPass : keyStorePass; + if (password != null) { + keyManagerFactory.init(_keyStore, password); + } else { + String ksName = + keyStoreProvider != null ? LocalizationMessages.SSL_KMF_NO_PASSWORD_FOR_PROVIDER_BASED_KS() + : keyStoreBytes != null ? LocalizationMessages.SSL_KMF_NO_PASSWORD_FOR_BYTE_BASED_KS() + : keyStoreFile; + + LOGGER.config(LocalizationMessages.SSL_KMF_NO_PASSWORD_SET(ksName)); + keyManagerFactory = null; + } + } catch (KeyStoreException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KMF_INIT_FAILED(), e); + } catch (UnrecoverableKeyException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KMF_UNRECOVERABLE_KEY(), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KMF_ALGORITHM_NOT_SUPPORTED(), e); + } catch (NoSuchProviderException e) { + throw new IllegalStateException(LocalizationMessages.SSL_KMF_PROVIDER_NOT_REGISTERED(), e); + } + } + + KeyStore _trustStore = trustStore; + if (_trustStore == null && (trustStoreBytes != null || trustStoreFile != null)) { + try { + if (trustStoreProvider != null) { + _trustStore = KeyStore.getInstance( + trustStoreType != null ? trustStoreType : KeyStore.getDefaultType(), trustStoreProvider); + } else { + _trustStore = + KeyStore.getInstance(trustStoreType != null ? trustStoreType : KeyStore.getDefaultType()); + } + InputStream trustStoreInputStream = null; + try { + if (trustStoreBytes != null) { + trustStoreInputStream = new ByteArrayInputStream(trustStoreBytes); + } else if (!trustStoreFile.equals("NONE")) { + trustStoreInputStream = new FileInputStream(trustStoreFile); + } + _trustStore.load(trustStoreInputStream, trustStorePass); + } finally { + try { + if (trustStoreInputStream != null) { + trustStoreInputStream.close(); + } + } catch (IOException ignored) { + } + } + } catch (KeyStoreException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TS_IMPL_NOT_FOUND(), e); + } catch (CertificateException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TS_CERT_LOAD_ERROR(), e); + } catch (FileNotFoundException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TS_FILE_NOT_FOUND(trustStoreFile), e); + } catch (IOException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TS_LOAD_ERROR(trustStoreFile), e); + } catch (NoSuchProviderException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TS_PROVIDERS_NOT_REGISTERED(), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TS_INTEGRITY_ALGORITHM_NOT_FOUND(), e); + } + } + if (_trustStore != null) { + String tmfAlgorithm = trustManagerFactoryAlgorithm; + if (tmfAlgorithm == null) { + tmfAlgorithm = AccessController.doPrivileged(PropertiesHelper.getSystemProperty( + TRUST_MANAGER_FACTORY_ALGORITHM, TrustManagerFactory.getDefaultAlgorithm())); + } + + try { + if (trustManagerFactoryProvider != null) { + trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm, trustManagerFactoryProvider); + } else { + trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm); + } + trustManagerFactory.init(_trustStore); + } catch (KeyStoreException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TMF_INIT_FAILED(), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TMF_ALGORITHM_NOT_SUPPORTED(), e); + } catch (NoSuchProviderException e) { + throw new IllegalStateException(LocalizationMessages.SSL_TMF_PROVIDER_NOT_REGISTERED(), e); + } + } + + try { + String secProtocol = "TLS"; + if (securityProtocol != null) { + secProtocol = securityProtocol; + } + final SSLContext sslContext = SSLContext.getInstance(secProtocol); + sslContext.init( + keyManagerFactory != null ? keyManagerFactory.getKeyManagers() : null, + trustManagerFactory != null ? trustManagerFactory.getTrustManagers() : null, + null); + return sslContext; + } catch (KeyManagementException e) { + throw new IllegalStateException(LocalizationMessages.SSL_CTX_INIT_FAILED(), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(LocalizationMessages.SSL_CTX_ALGORITHM_NOT_SUPPORTED(), e); + } + } + + /** + * Retrieve the SSL context configuration from the supplied properties. + * + * @param props properties containing the SSL context configuration. + * @return updated SSL configurator instance. + */ + public SslConfigurator retrieve(Properties props) { + trustStoreProvider = props.getProperty(TRUST_STORE_PROVIDER); + keyStoreProvider = props.getProperty(KEY_STORE_PROVIDER); + + trustManagerFactoryProvider = props.getProperty(TRUST_MANAGER_FACTORY_PROVIDER); + keyManagerFactoryProvider = props.getProperty(KEY_MANAGER_FACTORY_PROVIDER); + + trustStoreType = props.getProperty(TRUST_STORE_TYPE); + keyStoreType = props.getProperty(KEY_STORE_TYPE); + + if (props.getProperty(TRUST_STORE_PASSWORD) != null) { + trustStorePass = props.getProperty(TRUST_STORE_PASSWORD).toCharArray(); + } else { + trustStorePass = null; + } + + if (props.getProperty(KEY_STORE_PASSWORD) != null) { + keyStorePass = props.getProperty(KEY_STORE_PASSWORD).toCharArray(); + } else { + keyStorePass = null; + } + + trustStoreFile = props.getProperty(TRUST_STORE_FILE); + keyStoreFile = props.getProperty(KEY_STORE_FILE); + + trustStoreBytes = null; + keyStoreBytes = null; + + trustStore = null; + keyStore = null; + + securityProtocol = "TLS"; + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SslConfigurator that = (SslConfigurator) o; + + if (keyManagerFactoryAlgorithm != null + ? !keyManagerFactoryAlgorithm.equals(that.keyManagerFactoryAlgorithm) : that.keyManagerFactoryAlgorithm != null) { + return false; + } + if (keyManagerFactoryProvider != null + ? !keyManagerFactoryProvider.equals(that.keyManagerFactoryProvider) : that.keyManagerFactoryProvider != null) { + return false; + } + if (!Arrays.equals(keyPass, that.keyPass)) { + return false; + } + if (keyStore != null ? !keyStore.equals(that.keyStore) : that.keyStore != null) { + return false; + } + if (!Arrays.equals(keyStoreBytes, that.keyStoreBytes)) { + return false; + } + if (keyStoreFile != null ? !keyStoreFile.equals(that.keyStoreFile) : that.keyStoreFile != null) { + return false; + } + if (!Arrays.equals(keyStorePass, that.keyStorePass)) { + return false; + } + if (keyStoreProvider != null ? !keyStoreProvider.equals(that.keyStoreProvider) : that.keyStoreProvider != null) { + return false; + } + if (keyStoreType != null ? !keyStoreType.equals(that.keyStoreType) : that.keyStoreType != null) { + return false; + } + if (securityProtocol != null ? !securityProtocol.equals(that.securityProtocol) : that.securityProtocol != null) { + return false; + } + if (trustManagerFactoryAlgorithm != null ? !trustManagerFactoryAlgorithm.equals(that.trustManagerFactoryAlgorithm) + : that.trustManagerFactoryAlgorithm != null) { + return false; + } + if (trustManagerFactoryProvider != null ? !trustManagerFactoryProvider.equals(that.trustManagerFactoryProvider) + : that.trustManagerFactoryProvider != null) { + return false; + } + if (trustStore != null ? !trustStore.equals(that.trustStore) : that.trustStore != null) { + return false; + } + if (!Arrays.equals(trustStoreBytes, that.trustStoreBytes)) { + return false; + } + if (trustStoreFile != null ? !trustStoreFile.equals(that.trustStoreFile) : that.trustStoreFile != null) { + return false; + } + if (!Arrays.equals(trustStorePass, that.trustStorePass)) { + return false; + } + if (trustStoreProvider != null ? !trustStoreProvider.equals(that.trustStoreProvider) : that.trustStoreProvider != null) { + return false; + } + if (trustStoreType != null ? !trustStoreType.equals(that.trustStoreType) : that.trustStoreType != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = keyStore != null ? keyStore.hashCode() : 0; + result = 31 * result + (trustStore != null ? trustStore.hashCode() : 0); + result = 31 * result + (trustStoreProvider != null ? trustStoreProvider.hashCode() : 0); + result = 31 * result + (keyStoreProvider != null ? keyStoreProvider.hashCode() : 0); + result = 31 * result + (trustStoreType != null ? trustStoreType.hashCode() : 0); + result = 31 * result + (keyStoreType != null ? keyStoreType.hashCode() : 0); + result = 31 * result + (trustStorePass != null ? Arrays.hashCode(trustStorePass) : 0); + result = 31 * result + (keyStorePass != null ? Arrays.hashCode(keyStorePass) : 0); + result = 31 * result + (keyPass != null ? Arrays.hashCode(keyPass) : 0); + result = 31 * result + (trustStoreFile != null ? trustStoreFile.hashCode() : 0); + result = 31 * result + (keyStoreFile != null ? keyStoreFile.hashCode() : 0); + result = 31 * result + (trustStoreBytes != null ? Arrays.hashCode(trustStoreBytes) : 0); + result = 31 * result + (keyStoreBytes != null ? Arrays.hashCode(keyStoreBytes) : 0); + result = 31 * result + (trustManagerFactoryAlgorithm != null ? trustManagerFactoryAlgorithm.hashCode() : 0); + result = 31 * result + (keyManagerFactoryAlgorithm != null ? keyManagerFactoryAlgorithm.hashCode() : 0); + result = 31 * result + (trustManagerFactoryProvider != null ? trustManagerFactoryProvider.hashCode() : 0); + result = 31 * result + (keyManagerFactoryProvider != null ? keyManagerFactoryProvider.hashCode() : 0); + result = 31 * result + (securityProtocol != null ? securityProtocol.hashCode() : 0); + return result; + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java b/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java new file mode 100644 index 0000000..cc9669e --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/AbstractRuntimeDelegate.java
@@ -0,0 +1,119 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.net.URI; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.ext.RuntimeDelegate; + +import org.glassfish.jersey.message.internal.JerseyLink; +import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.message.internal.VariantListBuilder; +import org.glassfish.jersey.spi.HeaderDelegateProvider; +import org.glassfish.jersey.uri.internal.JerseyUriBuilder; + +/** + * An abstract implementation of {@link RuntimeDelegate} that + * provides support common to the client and server. + * + * @author Paul Sandoz + */ +public abstract class AbstractRuntimeDelegate extends RuntimeDelegate { + + private final Set<HeaderDelegateProvider> hps; + private final Map<Class<?>, HeaderDelegate<?>> map; + + /** + * Initialization constructor. The injection manager will be shut down. + * + * @param hps all {@link HeaderDelegateProvider} instances registered internally. + */ + protected AbstractRuntimeDelegate(Set<HeaderDelegateProvider> hps) { + this.hps = hps; + + /* + * Construct a map for quick look up of known header classes + */ + map = new WeakHashMap<>(); + map.put(EntityTag.class, _createHeaderDelegate(EntityTag.class)); + map.put(MediaType.class, _createHeaderDelegate(MediaType.class)); + map.put(CacheControl.class, _createHeaderDelegate(CacheControl.class)); + map.put(NewCookie.class, _createHeaderDelegate(NewCookie.class)); + map.put(Cookie.class, _createHeaderDelegate(Cookie.class)); + map.put(URI.class, _createHeaderDelegate(URI.class)); + map.put(Date.class, _createHeaderDelegate(Date.class)); + map.put(String.class, _createHeaderDelegate(String.class)); + } + + @Override + public javax.ws.rs.core.Variant.VariantListBuilder createVariantListBuilder() { + return new VariantListBuilder(); + } + + @Override + public ResponseBuilder createResponseBuilder() { + return new OutboundJaxrsResponse.Builder(new OutboundMessageContext()); + } + + @Override + public UriBuilder createUriBuilder() { + return new JerseyUriBuilder(); + } + + @Override + public Link.Builder createLinkBuilder() { + return new JerseyLink.Builder(); + } + + @Override + public <T> HeaderDelegate<T> createHeaderDelegate(final Class<T> type) { + if (type == null) { + throw new IllegalArgumentException("type parameter cannot be null"); + } + + @SuppressWarnings("unchecked") final HeaderDelegate<T> delegate = (HeaderDelegate<T>) map.get(type); + if (delegate != null) { + return delegate; + } + + return _createHeaderDelegate(type); + } + + @SuppressWarnings("unchecked") + private <T> HeaderDelegate<T> _createHeaderDelegate(final Class<T> type) { + for (final HeaderDelegateProvider hp : hps) { + if (hp.supports(type)) { + return hp; + } + } + + return null; + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/AbstractServiceFinderConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/internal/AbstractServiceFinderConfigurator.java new file mode 100644 index 0000000..2236d63 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/AbstractServiceFinderConfigurator.java
@@ -0,0 +1,74 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.ws.rs.RuntimeType; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.inject.InjectionManager; + +/** + * Simple ServiceFinder configuration. + * + * Looks for all implementations of a given contract using {@link ServiceFinder} and registers found instances to + * {@link InjectionManager}. + * + * @param <T> contract type. + * @author Petr Bouda + */ +public abstract class AbstractServiceFinderConfigurator<T> implements BootstrapConfigurator { + + private final Class<T> contract; + private final RuntimeType runtimeType; + + /** + * Create a new configurator. + * + * @param contract contract of the service providers bound by this binder. + * @param runtimeType runtime (client or server) where the service finder binder is used. + */ + protected AbstractServiceFinderConfigurator(Class<T> contract, RuntimeType runtimeType) { + this.contract = contract; + this.runtimeType = runtimeType; + } + + /** + * Load all particular implementations of the type {@code T} using {@link ServiceFinder}. + * + * @param applicationProperties map containing application properties. May be {@code null} + * @return all registered classes of the type {@code T}. + */ + protected List<Class<T>> loadImplementations(Map<String, Object> applicationProperties) { + boolean METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT = false; + boolean disableMetaInfServicesLookup = METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT; + if (applicationProperties != null) { + disableMetaInfServicesLookup = CommonProperties.getValue(applicationProperties, runtimeType, + CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT, Boolean.class); + } + if (!disableMetaInfServicesLookup) { + return Stream.of(ServiceFinder.find(contract, true).toClassArray()) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/AutoDiscoverableConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/internal/AutoDiscoverableConfigurator.java new file mode 100644 index 0000000..616818d --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/AutoDiscoverableConfigurator.java
@@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.ws.rs.RuntimeType; +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.spi.AutoDiscoverable; + +/** + * Configurator which initializes and register {@link AutoDiscoverable} instances into {@link InjectionManager} and + * {@link BootstrapBag}. + * + * @author Petr Bouda + */ +public class AutoDiscoverableConfigurator extends AbstractServiceFinderConfigurator<AutoDiscoverable> { + + /** + * Create a new configurator. + * + * @param runtimeType runtime (client or server) where the service finder binder is used. + */ + public AutoDiscoverableConfigurator(RuntimeType runtimeType) { + super(AutoDiscoverable.class, runtimeType); + } + + @Override + public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + Configuration configuration = bootstrapBag.getConfiguration(); + List<AutoDiscoverable> autoDiscoverables = loadImplementations(configuration.getProperties()).stream() + .peek(implClass -> injectionManager.register(Bindings.service(implClass).to(AutoDiscoverable.class))) + .map(injectionManager::createAndInitialize) + .collect(Collectors.toList()); + + bootstrapBag.setAutoDiscoverables(autoDiscoverables); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/BootstrapBag.java b/core-common/src/main/java/org/glassfish/jersey/internal/BootstrapBag.java new file mode 100644 index 0000000..53309fb --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/BootstrapBag.java
@@ -0,0 +1,190 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Objects; + +import javax.ws.rs.core.Configuration; + +import org.glassfish.jersey.internal.spi.AutoDiscoverable; +import org.glassfish.jersey.message.MessageBodyWorkers; +import org.glassfish.jersey.model.internal.ManagedObjectsFinalizer; +import org.glassfish.jersey.process.internal.RequestScope; +import org.glassfish.jersey.spi.ContextResolvers; +import org.glassfish.jersey.spi.ExceptionMappers; + +/** + * A holder that is used only during Jersey bootstrap to keep the instances of the given types and then use them during the + * bootstrap. This works as a replacement of an injection framework during a bootstrap and intentionally keeps all needed types in + * separate fields to make strong type nature and to preserve a clear view which types are needed to inject to other services. + * + * @author Petr Bouda + */ +public class BootstrapBag { + + private Configuration configuration; + private RequestScope requestScope; + private MessageBodyWorkers messageBodyWorkers; + private ExceptionMappers exceptionMappers; + private ContextResolvers contextResolvers; + private ManagedObjectsFinalizer managedObjectsFinalizer; + private List<AutoDiscoverable> autoDiscoverables; + + /** + * Gets a list of {@link AutoDiscoverable}. + * + * @return list of {@link AutoDiscoverable}. + */ + public List<AutoDiscoverable> getAutoDiscoverables() { + return autoDiscoverables; + } + + /** + * Sets a list of {@link AutoDiscoverable}. + * + * @param autoDiscoverables list of {@code AutoDiscoverable}. + */ + public void setAutoDiscoverables(List<AutoDiscoverable> autoDiscoverables) { + this.autoDiscoverables = autoDiscoverables; + } + + /** + * Gets an instance of {@link ManagedObjectsFinalizer}. + * + * @return {@code ManagedObjectsFinalizer} instance. + */ + public ManagedObjectsFinalizer getManagedObjectsFinalizer() { + return managedObjectsFinalizer; + } + + /** + * Sets an instance of {@link ManagedObjectsFinalizer}. + * + * @param managedObjectsFinalizer {@code ManagedObjectsFinalizer} instance. + */ + public void setManagedObjectsFinalizer(ManagedObjectsFinalizer managedObjectsFinalizer) { + this.managedObjectsFinalizer = managedObjectsFinalizer; + } + + /** + * Gets an instance of {@link RequestScope}. + * + * @return {@code RequestScope} instance. + */ + public RequestScope getRequestScope() { + requireNonNull(requestScope, RequestScope.class); + return requestScope; + } + + /** + * Sets an instance of {@link RequestScope}. + * + * @param requestScope {@code RequestScope} instance. + */ + public void setRequestScope(RequestScope requestScope) { + this.requestScope = requestScope; + } + + /** + * Gets an instance of {@link MessageBodyWorkers}. + * + * @return {@code MessageBodyWorkers} instance. + */ + public MessageBodyWorkers getMessageBodyWorkers() { + requireNonNull(messageBodyWorkers, MessageBodyWorkers.class); + return messageBodyWorkers; + } + + /** + * Sets an instance of {@link MessageBodyWorkers}. + * + * @param messageBodyWorkers {@code MessageBodyWorkers} instance. + */ + public void setMessageBodyWorkers(MessageBodyWorkers messageBodyWorkers) { + this.messageBodyWorkers = messageBodyWorkers; + } + + /** + * Gets an instance of {@link Configuration}. + * + * @return {@code Configuration} instance. + */ + public Configuration getConfiguration() { + requireNonNull(configuration, Configuration.class); + return configuration; + } + + /** + * Sets an instance of {@link Configuration}. + * + * @param configuration {@code Configuration} instance. + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Gets an instance of {@link ExceptionMappers}. + * + * @return {@code ExceptionMappers} instance. + */ + public ExceptionMappers getExceptionMappers() { + requireNonNull(exceptionMappers, ExceptionMappers.class); + return exceptionMappers; + } + + /** + * Sets an instance of {@link ExceptionMappers}. + * + * @param exceptionMappers {@code ExceptionMappers} instance. + */ + public void setExceptionMappers(ExceptionMappers exceptionMappers) { + this.exceptionMappers = exceptionMappers; + } + + /** + * Gets an instance of {@link ContextResolvers}. + * + * @return {@code ContextResolvers} instance. + */ + public ContextResolvers getContextResolvers() { + requireNonNull(contextResolvers, ContextResolvers.class); + return contextResolvers; + } + + /** + * Sets an instance of {@link ContextResolvers}. + * + * @param contextResolvers {@code ContextResolvers} instance. + */ + public void setContextResolvers(ContextResolvers contextResolvers) { + this.contextResolvers = contextResolvers; + } + + /** + * Check whether the value is not {@code null} that means that the proper {@link BootstrapConfigurator} has not been configured + * or in a wrong order. + * + * @param object tested object. + * @param type type of the tested object. + */ + protected static void requireNonNull(Object object, Type type) { + Objects.requireNonNull(object, type + " has not been added into BootstrapBag yet"); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/BootstrapConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/internal/BootstrapConfigurator.java new file mode 100644 index 0000000..a316453 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/BootstrapConfigurator.java
@@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import org.glassfish.jersey.internal.inject.InjectionManager; + +/** + * Configurator which contains two methods, {@link #init(InjectionManager, BootstrapBag)} contains {@link InjectionManager} + * into which only registering services make sense because injection manager has not been completed yet and + * {@link #postInit(InjectionManager, BootstrapBag)} in which {@link InjectionManager} has been already completed and is able to + * create and provide services. + * <p> + * The configurators should register instances into {@link InjectionManager} only if the instance must be really injectable if + * the instance can be used internally without the injection, then extend {@link BootstrapBag} and propagate the instance to + * correct services using constructors or methods in a phase of Jersey initialization. + * + * @author Petr Bouda + */ +public interface BootstrapConfigurator { + + /** + * Pre-initialization method should only register services into {@link InjectionManager} and populate {@link BootstrapBag}. + * + * @param injectionManager not completed injection manager. + * @param bootstrapBag bootstrap bag with services used in following processing. + */ + void init(InjectionManager injectionManager, BootstrapBag bootstrapBag); + + /** + * Post-initialization method can get services from {@link InjectionManager} and is not able to register the new one because + * injection manager is already completed. + * + * @param injectionManager already completed injection manager. + * @param bootstrapBag bootstrap bag with services used in following processing. + */ + default void postInit(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + } + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/ContextResolverFactory.java b/core-common/src/main/java/org/glassfish/jersey/internal/ContextResolverFactory.java new file mode 100644 index 0000000..aa97add --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/ContextResolverFactory.java
@@ -0,0 +1,241 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ContextResolver; + +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InstanceBinding; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.ReflectionHelper.DeclaringClassInterfacePair; +import org.glassfish.jersey.internal.util.collection.KeyComparatorHashMap; +import org.glassfish.jersey.message.internal.MediaTypes; +import org.glassfish.jersey.message.internal.MessageBodyFactory; +import org.glassfish.jersey.spi.ContextResolvers; + +/** + * A factory implementation for managing {@link ContextResolver} instances. + * + * @author Paul Sandoz + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ContextResolverFactory implements ContextResolvers { + + /** + * Configurator which initializes and register {@link ContextResolvers} instance into {@link InjectionManager} and + * {@link BootstrapBag}. + * + * @author Petr Bouda + */ + public static class ContextResolversConfigurator implements BootstrapConfigurator { + + private ContextResolverFactory contextResolverFactory; + + @Override + public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + contextResolverFactory = new ContextResolverFactory(); + InstanceBinding<ContextResolverFactory> binding = + Bindings.service(contextResolverFactory) + .to(ContextResolvers.class); + injectionManager.register(binding); + } + + @Override + public void postInit(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + contextResolverFactory.initialize(injectionManager.getAllInstances(ContextResolver.class)); + bootstrapBag.setContextResolvers(contextResolverFactory); + } + } + + private final Map<Type, Map<MediaType, ContextResolver>> resolver = new HashMap<>(3); + private final Map<Type, ConcurrentHashMap<MediaType, ContextResolver>> cache = new HashMap<>(3); + + /** + * Private constructor to allow to create {@link ContextResolverFactory} only in {@link ContextResolversConfigurator}. + */ + private ContextResolverFactory(){ + } + + private void initialize(List<ContextResolver> contextResolvers) { + Map<Type, Map<MediaType, List<ContextResolver>>> rs = new HashMap<>(); + + for (ContextResolver provider : contextResolvers) { + List<MediaType> ms = MediaTypes.createFrom(provider.getClass().getAnnotation(Produces.class)); + Type type = getParameterizedType(provider.getClass()); + + Map<MediaType, List<ContextResolver>> mr = rs.get(type); + if (mr == null) { + mr = new HashMap<>(); + rs.put(type, mr); + } + for (MediaType m : ms) { + List<ContextResolver> crl = mr.get(m); + if (crl == null) { + crl = new ArrayList<>(); + mr.put(m, crl); + } + crl.add(provider); + } + } + + // Reduce set of two or more context resolvers for same type and + // media type + + for (Map.Entry<Type, Map<MediaType, List<ContextResolver>>> e : rs.entrySet()) { + Map<MediaType, ContextResolver> mr = new KeyComparatorHashMap<>(4, MessageBodyFactory.MEDIA_TYPE_KEY_COMPARATOR); + resolver.put(e.getKey(), mr); + cache.put(e.getKey(), new ConcurrentHashMap<>(4)); + + for (Map.Entry<MediaType, List<ContextResolver>> f : e.getValue().entrySet()) { + mr.put(f.getKey(), reduce(f.getValue())); + } + } + } + + private Type getParameterizedType(final Class<?> c) { + final DeclaringClassInterfacePair p = ReflectionHelper.getClass( + c, ContextResolver.class); + + final Type[] as = ReflectionHelper.getParameterizedTypeArguments(p); + + return (as != null) ? as[0] : Object.class; + } + + private static final NullContextResolverAdapter NULL_CONTEXT_RESOLVER = + new NullContextResolverAdapter(); + + private static final class NullContextResolverAdapter implements ContextResolver { + + @Override + public Object getContext(final Class type) { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + private static final class ContextResolverAdapter implements ContextResolver { + + private final ContextResolver[] cra; + + ContextResolverAdapter(final ContextResolver... cra) { + this(removeNull(cra)); + } + + ContextResolverAdapter(final List<ContextResolver> crl) { + this.cra = crl.toArray(new ContextResolver[crl.size()]); + } + + @Override + public Object getContext(final Class objectType) { + for (final ContextResolver cr : cra) { + @SuppressWarnings("unchecked") final Object c = cr.getContext(objectType); + if (c != null) { + return c; + } + } + return null; + } + + ContextResolver reduce() { + if (cra.length == 0) { + return NULL_CONTEXT_RESOLVER; + } + if (cra.length == 1) { + return cra[0]; + } else { + return this; + } + } + + private static List<ContextResolver> removeNull(final ContextResolver... cra) { + final List<ContextResolver> crl = new ArrayList<>(cra.length); + for (final ContextResolver cr : cra) { + if (cr != null) { + crl.add(cr); + } + } + return crl; + } + } + + private ContextResolver reduce(final List<ContextResolver> r) { + if (r.size() == 1) { + return r.iterator().next(); + } else { + return new ContextResolverAdapter(r); + } + } + + @SuppressWarnings("unchecked") + @Override + public <T> ContextResolver<T> resolve(final Type t, MediaType m) { + final ConcurrentHashMap<MediaType, ContextResolver> crMapCache = cache.get(t); + if (crMapCache == null) { + return null; + } + + if (m == null) { + m = MediaType.WILDCARD_TYPE; + } + + ContextResolver<T> cr = crMapCache.get(m); + if (cr == null) { + final Map<MediaType, ContextResolver> crMap = resolver.get(t); + + if (m.isWildcardType()) { + cr = crMap.get(MediaType.WILDCARD_TYPE); + if (cr == null) { + cr = NULL_CONTEXT_RESOLVER; + } + } else if (m.isWildcardSubtype()) { + // Include x, x/* and */* + final ContextResolver<T> subTypeWildCard = crMap.get(m); + final ContextResolver<T> wildCard = crMap.get(MediaType.WILDCARD_TYPE); + + cr = new ContextResolverAdapter(subTypeWildCard, wildCard).reduce(); + } else { + // Include x, x/* and */* + final ContextResolver<T> type = crMap.get(m); + final ContextResolver<T> subTypeWildCard = crMap.get(new MediaType(m.getType(), "*")); + final ContextResolver<T> wildCard = crMap.get(MediaType.WILDCARD_TYPE); + + cr = new ContextResolverAdapter(type, subTypeWildCard, wildCard).reduce(); + } + + final ContextResolver<T> _cr = crMapCache.putIfAbsent(m, cr); + // If there is already a value in the cache use that + // instance, and discard the new and redundant instance, to + // ensure the same instance is always returned. + // The cached instance and the new instance will have the same + // functionality. + if (_cr != null) { + cr = _cr; + } + } + + return (cr != NULL_CONTEXT_RESOLVER) ? cr : null; + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/Errors.java b/core-common/src/main/java/org/glassfish/jersey/internal/Errors.java new file mode 100644 index 0000000..44fad3c --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/Errors.java
@@ -0,0 +1,518 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.logging.Logger; + +import org.glassfish.jersey.Severity; +import org.glassfish.jersey.internal.util.Producer; + +/** + * Errors utility used to file processing messages (e.g. validation, provider, resource building errors, hint). + * <p/> + * Error filing methods ({@code #warning}, {@code #error}, {@code #fatal}) can be invoked only in the "error scope" which is + * created by {@link #process(Producer)} or + * {@link #processWithException(Producer)} methods. Filed error messages are present also in this + * scope. + * <p/> + * TODO do not use static thread local? + * + * @author Michal Gajdos + */ +public class Errors { + + private static final Logger LOGGER = Logger.getLogger(Errors.class.getName()); + + private static final ThreadLocal<Errors> errors = new ThreadLocal<Errors>(); + + /** + * Add an error to the list of messages. + * + * @param message message of the error. + * @param severity indicates severity of added error. + */ + public static void error(final String message, Severity severity) { + error(null, message, severity); + } + + /** + * Add an error to the list of messages. + * + * @param source source of the error. + * @param message message of the error. + * @param severity indicates severity of added error. + */ + public static void error(final Object source, final String message, final Severity severity) { + getInstance().issues.add(new ErrorMessage(source, message, severity)); + } + + /** + * Add a fatal error to the list of messages. + * + * @param source source of the error. + * @param message message of the error. + */ + public static void fatal(final Object source, final String message) { + error(source, message, Severity.FATAL); + } + + /** + * Add a warning to the list of messages. + * + * @param source source of the error. + * @param message message of the error. + */ + public static void warning(final Object source, final String message) { + error(source, message, Severity.WARNING); + } + + /** + * Add a hint to the list of messages. + * + * @param source source of the error. + * @param message message of the error. + */ + public static void hint(final Object source, final String message) { + getInstance().issues.add(new ErrorMessage(source, message, Severity.HINT)); + } + + /** + * Log errors and throw an exception if there are any fatal issues detected and + * the {@code throwException} flag has been set to {@code true}. + * + * @param throwException if set to {@code true}, any fatal issues will cause a {@link ErrorMessagesException} + * to be thrown. + */ + private static void processErrors(final boolean throwException) { + final List<ErrorMessage> errors = new ArrayList<ErrorMessage>(Errors.errors.get().issues); + boolean isFatal = logErrors(errors); + if (throwException && isFatal) { + throw new ErrorMessagesException(errors); + } + } + + /** + * Log errors and return a status flag indicating whether a fatal issue has been found + * in the error collection. + * <p> + * The {@code afterMark} flag indicates whether only those issues should be logged that were + * added after a {@link #mark() mark has been set}. + * </p> + * + * @param afterMark if {@code true}, only issues added after a mark has been set are returned, + * if {@code false} all issues are returned. + * @return {@code true} if there are any fatal issues present in the collection, {@code false} + * otherwise. + */ + public static boolean logErrors(final boolean afterMark) { + return logErrors(getInstance()._getErrorMessages(afterMark)); + } + + /** + * Log supplied errors and return a status flag indicating whether a fatal issue has been found + * in the error collection. + * + * @param errors a collection of errors to be logged. + * @return {@code true} if there are any fatal issues present in the collection, {@code false} + * otherwise. + */ + private static boolean logErrors(final Collection<ErrorMessage> errors) { + boolean isFatal = false; + + if (!errors.isEmpty()) { + StringBuilder fatals = new StringBuilder("\n"); + StringBuilder warnings = new StringBuilder(); + StringBuilder hints = new StringBuilder(); + + for (final ErrorMessage error : errors) { + switch (error.getSeverity()) { + case FATAL: + isFatal = true; + fatals.append(LocalizationMessages.ERROR_MSG(error.getMessage())).append('\n'); + break; + case WARNING: + warnings.append(LocalizationMessages.WARNING_MSG(error.getMessage())).append('\n'); + break; + case HINT: + hints.append(LocalizationMessages.HINT_MSG(error.getMessage())).append('\n'); + break; + } + } + + if (isFatal) { + LOGGER.severe(LocalizationMessages.ERRORS_AND_WARNINGS_DETECTED(fatals.append(warnings) + .append(hints).toString())); + } else { + if (warnings.length() > 0) { + LOGGER.warning(LocalizationMessages.WARNINGS_DETECTED(warnings.toString())); + } + + if (hints.length() > 0) { + LOGGER.config(LocalizationMessages.HINTS_DETECTED(hints.toString())); + } + } + } + + return isFatal; + } + + + /** + * Check whether a fatal error is present in the list of all messages. + * + * @return {@code true} if there are any fatal issues in this error context, {@code false} otherwise. + */ + public static boolean fatalIssuesFound() { + for (final ErrorMessage message : getInstance().issues) { + if (message.getSeverity() == Severity.FATAL) { + return true; + } + } + return false; + } + + /** + * Invoke given producer task and gather errors. + * <p/> + * After the task is complete all gathered errors are logged. No exception is thrown + * even if there is a fatal error present in the list of errors. + * + * @param producer producer task to be invoked. + * @return the result produced by the task. + */ + public static <T> T process(final Producer<T> producer) { + return process(producer, false); + } + + /** + * Invoke given callable task and gather messages. + * <p/> + * After the task is complete all gathered errors are logged. Any exception thrown + * by the throwable is re-thrown. + * + * @param task callable task to be invoked. + * @return the result produced by the task. + * @throws Exception exception thrown by the task. + */ + public static <T> T process(final Callable<T> task) throws Exception { + return process(task, true); + } + + /** + * Invoke given producer task and gather messages. + * <p/> + * After the task is complete all gathered errors are logged. If there is a fatal error + * present in the list of errors an {@link ErrorMessagesException exception} is thrown. + * + * @param producer producer task to be invoked. + * @return the result produced by the task. + */ + public static <T> T processWithException(final Producer<T> producer) { + return process(producer, true); + } + + /** + * Invoke given task and gather messages. + * <p/> + * After the task is complete all gathered errors are logged. No exception is thrown + * even if there is a fatal error present in the list of errors. + * + * @param task task to be invoked. + */ + public static void process(final Runnable task) { + process(new Producer<Void>() { + + @Override + public Void call() { + task.run(); + return null; + } + }, false); + } + + /** + * Invoke given task and gather messages. + * <p/> + * After the task is complete all gathered errors are logged. If there is a fatal error + * present in the list of errors an {@link ErrorMessagesException exception} is thrown. + * + * @param task task to be invoked. + */ + public static void processWithException(final Runnable task) { + process(new Producer<Void>() { + @Override + public Void call() { + task.run(); + return null; + } + }, true); + } + + private static <T> T process(final Producer<T> task, final boolean throwException) { + try { + return process((Callable<T>) task, throwException); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private static <T> T process(final Callable<T> task, final boolean throwException) throws Exception { + Errors instance = errors.get(); + if (instance == null) { + instance = new Errors(); + errors.set(instance); + } + instance.preProcess(); + + Exception caught = null; + try { + return task.call(); + } catch (Exception re) { + // If a runtime exception is caught then report errors and rethrow. + caught = re; + } finally { + instance.postProcess(throwException && caught == null); + } + + throw caught; + } + + private static Errors getInstance() { + final Errors instance = errors.get(); + // No error processing in scope + if (instance == null) { + throw new IllegalStateException(LocalizationMessages.NO_ERROR_PROCESSING_IN_SCOPE()); + } + // The following should not be necessary but given the fragile nature of + // static thread local probably best to add it in case some internals of + // this class change + if (instance.stack == 0) { + errors.remove(); + throw new IllegalStateException(LocalizationMessages.NO_ERROR_PROCESSING_IN_SCOPE()); + } + return instance; + } + + /** + * Get the list of all error messages. + * + * @return non-null error message list. + */ + public static List<ErrorMessage> getErrorMessages() { + return getErrorMessages(false); + } + + /** + * Get the list of error messages. + * <p> + * The {@code afterMark} flag indicates whether only those issues should be returned that were + * added after a {@link #mark() mark has been set}. + * </p> + * + * @param afterMark if {@code true}, only issues added after a mark has been set are returned, + * if {@code false} all issues are returned. + * @return non-null error list. + */ + public static List<ErrorMessage> getErrorMessages(final boolean afterMark) { + return getInstance()._getErrorMessages(afterMark); + } + + /** + * Set a mark at a current position in the errors messages list. + */ + public static void mark() { + getInstance()._mark(); + } + + /** + * Remove a previously set mark, if any. + */ + public static void unmark() { + getInstance()._unmark(); + } + + /** + * Removes all issues that have been added since the last marked position as well as + * removes the last mark. + */ + public static void reset() { + getInstance()._reset(); + } + + private final ArrayList<ErrorMessage> issues = new ArrayList<ErrorMessage>(0); + + private Errors() { + } + + private Deque<Integer> mark = new ArrayDeque<Integer>(4); + private int stack = 0; + + private void _mark() { + mark.addLast(issues.size()); + } + + private void _unmark() { + mark.pollLast(); + } + + private void _reset() { + final Integer _pos = mark.pollLast(); // also performs "unmark" functionality + final int markedPos = (_pos == null) ? -1 : _pos; + + if (markedPos >= 0 && markedPos < issues.size()) { + issues.subList(markedPos, issues.size()).clear(); + } + } + + private void preProcess() { + stack++; + } + + private void postProcess(boolean throwException) { + stack--; + + if (stack == 0) { + try { + if (!issues.isEmpty()) { + processErrors(throwException); + } + } finally { + errors.remove(); + } + } + } + + private List<ErrorMessage> _getErrorMessages(final boolean afterMark) { + if (afterMark) { + final Integer _pos = mark.peekLast(); + final int markedPos = (_pos == null) ? -1 : _pos; + + if (markedPos >= 0 && markedPos < issues.size()) { + return Collections.unmodifiableList(new ArrayList<ErrorMessage>(issues.subList(markedPos, issues.size()))); + } // else return all errors + } + + return Collections.unmodifiableList(new ArrayList<ErrorMessage>(issues)); + } + + /** + * Error message exception. + */ + public static class ErrorMessagesException extends RuntimeException { + + private final List<ErrorMessage> messages; + + private ErrorMessagesException(final List<ErrorMessage> messages) { + this.messages = messages; + } + + /** + * Get encountered error messages. + * + * @return encountered error messages. + */ + public List<ErrorMessage> getMessages() { + return messages; + } + } + + /** + * Generic error message. + */ + public static class ErrorMessage { + + private final Object source; + private final String message; + private final Severity severity; + + private ErrorMessage(final Object source, final String message, Severity severity) { + this.source = source; + this.message = message; + this.severity = severity; + } + + /** + * Get {@link Severity}. + * + * @return severity of current {@code ErrorMessage}. + */ + public Severity getSeverity() { + return severity; + } + + /** + * Human-readable description of the issue. + * + * @return message describing the issue. + */ + public String getMessage() { + return message; + } + + /** + * The issue source. + * <p/> + * Identifies the object where the issue was found. + * + * @return source of the issue. + */ + public Object getSource() { + return source; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ErrorMessage that = (ErrorMessage) o; + + if (message != null ? !message.equals(that.message) : that.message != null) { + return false; + } + if (severity != that.severity) { + return false; + } + if (source != null ? !source.equals(that.source) : that.source != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = source != null ? source.hashCode() : 0; + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (severity != null ? severity.hashCode() : 0); + return result; + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/ExceptionMapperFactory.java b/core-common/src/main/java/org/glassfish/jersey/internal/ExceptionMapperFactory.java new file mode 100644 index 0000000..aff86bd --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/ExceptionMapperFactory.java
@@ -0,0 +1,308 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.ext.ExceptionMapper; + +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InstanceBinding; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.inject.ServiceHolder; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.ClassTypePair; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; +import org.glassfish.jersey.spi.ExceptionMappers; +import org.glassfish.jersey.spi.ExtendedExceptionMapper; + +/** + * {@link ExceptionMappers Exception mappers} implementation that aggregates + * exception mappers and server as the main entry point for exception mapper + * instance lookup. + * + * @author Paul Sandoz + * @author Santiago Pericas-Geertsen (Santiago.PericasGeertsen at oracle.com) + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + */ +public class ExceptionMapperFactory implements ExceptionMappers { + + private static final Logger LOGGER = Logger.getLogger(ExceptionMapperFactory.class.getName()); + + /** + * Configurator which initializes and register {@link ExceptionMappers} instance into {@link InjectionManager} and + * {@link BootstrapBag}. + * + * @author Petr Bouda + */ + public static class ExceptionMappersConfigurator implements BootstrapConfigurator { + + private ExceptionMapperFactory exceptionMapperFactory; + + @Override + public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + exceptionMapperFactory = new ExceptionMapperFactory(injectionManager); + InstanceBinding<ExceptionMapperFactory> binding = + Bindings.service(exceptionMapperFactory) + .to(ExceptionMappers.class); + injectionManager.register(binding); + } + + @Override + public void postInit(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + bootstrapBag.setExceptionMappers(exceptionMapperFactory); + } + } + + private static class ExceptionMapperType { + + ServiceHolder<ExceptionMapper> mapper; + Class<? extends Throwable> exceptionType; + + public ExceptionMapperType(final ServiceHolder<ExceptionMapper> mapper, final Class<? extends Throwable> exceptionType) { + this.mapper = mapper; + this.exceptionType = exceptionType; + } + } + + private final Value<Set<ExceptionMapperType>> exceptionMapperTypes; + + @Override + @SuppressWarnings("unchecked") + public <T extends Throwable> ExceptionMapper<T> findMapping(final T exceptionInstance) { + return find((Class<T>) exceptionInstance.getClass(), exceptionInstance); + } + + @Override + public <T extends Throwable> ExceptionMapper<T> find(final Class<T> type) { + return find(type, null); + } + + @SuppressWarnings("unchecked") + private <T extends Throwable> ExceptionMapper<T> find(final Class<T> type, final T exceptionInstance) { + ExceptionMapper<T> mapper = null; + int minDistance = Integer.MAX_VALUE; + + for (final ExceptionMapperType mapperType : exceptionMapperTypes.get()) { + final int d = distance(type, mapperType.exceptionType); + if (d >= 0 && d <= minDistance) { + final ExceptionMapper<T> candidate = mapperType.mapper.getInstance(); + + if (isPreferredCandidate(exceptionInstance, candidate, d == minDistance)) { + mapper = candidate; + minDistance = d; + if (d == 0) { + // slight optimization: if the distance is 0, it is already the best case, so we can exit + return mapper; + } + } + } + } + return mapper; + } + + /** + * Determines whether the currently considered candidate should be preferred over the previous one. + * + * @param exceptionInstance exception to be mapped. + * @param candidate mapper able to map given exception type. + * @param sameDistance flag indicating whether this and the previously considered candidate are in the same distance. + * @param <T> exception type. + * @return {@code true} if the given candidate is preferred over the previous one with the same or lower distance, + * {@code false} otherwise. + */ + private <T extends Throwable> boolean isPreferredCandidate(final T exceptionInstance, final ExceptionMapper<T> candidate, + final boolean sameDistance) { + if (exceptionInstance == null) { + return true; + } + if (candidate instanceof ExtendedExceptionMapper) { + return !sameDistance + && ((ExtendedExceptionMapper<T>) candidate).isMappable(exceptionInstance); + } else { + return !sameDistance; + } + } + + /** + * Create new exception mapper factory initialized with {@link InjectionManager injection manager} + * instance that will be used to look up all providers implementing {@link ExceptionMapper} interface. + * + * @param injectionManager injection manager. + */ + public ExceptionMapperFactory(InjectionManager injectionManager) { + exceptionMapperTypes = createLazyExceptionMappers(injectionManager); + } + + /** + * Returns {@link LazyValue} of exception mappers that delays their creation to the first use. The exception mappers won't be + * created during bootstrap but at the time of the first call. + * + * @param injectionManager injection manager that may not be fully populated at the time of a function call therefore the + * result is wrapped to lazy value. + * @return lazy value of exception mappers. + */ + private LazyValue<Set<ExceptionMapperType>> createLazyExceptionMappers(InjectionManager injectionManager) { + return Values.lazy((Value<Set<ExceptionMapperType>>) () -> { + Collection<ServiceHolder<ExceptionMapper>> mapperHandles = + Providers.getAllServiceHolders(injectionManager, ExceptionMapper.class); + + Set<ExceptionMapperType> exceptionMapperTypes = new LinkedHashSet<>(); + for (ServiceHolder<ExceptionMapper> mapperHandle: mapperHandles) { + ExceptionMapper mapper = mapperHandle.getInstance(); + + if (Proxy.isProxyClass(mapper.getClass())) { + SortedSet<Class<? extends ExceptionMapper>> mapperTypes = + new TreeSet<>((o1, o2) -> o1.isAssignableFrom(o2) ? -1 : 1); + + Set<Type> contracts = mapperHandle.getContractTypes(); + for (final Type contract : contracts) { + if (contract instanceof Class + && ExceptionMapper.class.isAssignableFrom((Class<?>) contract) + && contract != ExceptionMapper.class) { + //noinspection unchecked + mapperTypes.add((Class<? extends ExceptionMapper>) contract); + } + } + + if (!mapperTypes.isEmpty()) { + final Class<? extends Throwable> c = getExceptionType(mapperTypes.first()); + if (c != null) { + exceptionMapperTypes.add(new ExceptionMapperType(mapperHandle, c)); + } + } + } else { + final Class<? extends Throwable> c = getExceptionType(mapper.getClass()); + if (c != null) { + exceptionMapperTypes.add(new ExceptionMapperType(mapperHandle, c)); + } + } + } + return exceptionMapperTypes; + }); + } + + private int distance(Class<?> c, final Class<?> emtc) { + int distance = 0; + if (!emtc.isAssignableFrom(c)) { + return -1; + } + + while (c != emtc) { + c = c.getSuperclass(); + distance++; + } + + return distance; + } + + @SuppressWarnings("unchecked") + private Class<? extends Throwable> getExceptionType(final Class<? extends ExceptionMapper> c) { + final Class<?> t = getType(c); + if (Throwable.class.isAssignableFrom(t)) { + return (Class<? extends Throwable>) t; + } + + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.warning(LocalizationMessages.EXCEPTION_MAPPER_SUPPORTED_TYPE_UNKNOWN(c.getName())); + } + + return null; + } + + /** + * Get exception type for given exception mapper class. + * + * @param clazz class to get exception type for. + * @return exception type for given class. + */ + private Class getType(final Class<? extends ExceptionMapper> clazz) { + Class clazzHolder = clazz; + + while (clazzHolder != Object.class) { + final Class type = getTypeFromInterface(clazzHolder, clazz); + if (type != null) { + return type; + } + + clazzHolder = clazzHolder.getSuperclass(); + } + + throw new ProcessingException(LocalizationMessages.ERROR_FINDING_EXCEPTION_MAPPER_TYPE(clazz)); + } + + /** + * Iterate through interface hierarchy of {@code clazz} and get exception type for given class. + * + * @param clazz class to inspect. + * @return exception type for given class or {@code null} if the class doesn't implement {@code ExceptionMapper}. + */ + private Class getTypeFromInterface(Class<?> clazz, final Class<? extends ExceptionMapper> original) { + final Type[] types = clazz.getGenericInterfaces(); + + for (final Type type : types) { + if (type instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) type; + if (pt.getRawType() == ExceptionMapper.class + || pt.getRawType() == ExtendedExceptionMapper.class) { + return getResolvedType(pt.getActualTypeArguments()[0], original, clazz); + } + } else if (type instanceof Class<?>) { + clazz = (Class<?>) type; + + if (ExceptionMapper.class.isAssignableFrom(clazz)) { + return getTypeFromInterface(clazz, original); + } + } + } + + return null; + } + + private Class getResolvedType(final Type t, final Class c, final Class dc) { + if (t instanceof Class) { + return (Class) t; + } else if (t instanceof TypeVariable) { + final ClassTypePair ct = ReflectionHelper.resolveTypeVariable(c, dc, (TypeVariable) t); + if (ct != null) { + return ct.rawClass(); + } else { + return null; + } + } else if (t instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) t; + return (Class) pt.getRawType(); + } else { + return null; + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/InternalProperties.java b/core-common/src/main/java/org/glassfish/jersey/internal/InternalProperties.java new file mode 100644 index 0000000..75f85ed --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/InternalProperties.java
@@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import org.glassfish.jersey.internal.util.PropertiesClass; + +/** + * Internal common (server/client) Jersey configuration properties. + * + * @author Michal Gajdos + */ +@PropertiesClass +public class InternalProperties { + + /** + * This property should be set by configured JSON feature to indicate that other (registered but not configured) JSON features + * should not be configured. + * <p/> + * The name of the configuration property is <tt>{@value}</tt>. + * + * @since 2.9 + */ + public static final String JSON_FEATURE = "jersey.config.jsonFeature"; + + /** + * Client-specific version of {@link InternalProperties#JSON_FEATURE}. + * <p/> + * If present, it overrides the generic one for the client environment. + * + * @since 2.9 + */ + public static final String JSON_FEATURE_CLIENT = "jersey.config.client.jsonFeature"; + + /** + * Server-specific version of {@link InternalProperties#JSON_FEATURE}. + * <p/> + * If present, it overrides the generic one for the server environment. + * + * @since 2.9 + */ + public static final String JSON_FEATURE_SERVER = "jersey.config.server.jsonFeature"; + + /** + * Prevent instantiation. + */ + private InternalProperties() { + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/JaxrsProviders.java b/core-common/src/main/java/org/glassfish/jersey/internal/JaxrsProviders.java new file mode 100644 index 0000000..519b1e5 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/JaxrsProviders.java
@@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Providers; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.glassfish.jersey.internal.inject.Bindings; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.PerLookup; +import org.glassfish.jersey.message.MessageBodyWorkers; +import org.glassfish.jersey.spi.ContextResolvers; +import org.glassfish.jersey.spi.ExceptionMappers; + +/** + * Jersey implementation of JAX-RS {@link Providers} contract. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class JaxrsProviders implements Providers { + + /** + * Configurator which initializes and registers {@link Providers} instance into {@link InjectionManager} and + * {@link BootstrapBag}. + * Instances of these interfaces are processed, configured and provided using this configurator: + * <ul> + * <li>{@link Providers}</li> + * </ul> + */ + public static class ProvidersConfigurator implements BootstrapConfigurator{ + + @Override + public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) { + injectionManager.register( + Bindings.service(JaxrsProviders.class) + .to(Providers.class) + .in(PerLookup.class)); + } + } + + @Inject + private Provider<MessageBodyWorkers> workers; + @Inject + private Provider<ContextResolvers> resolvers; + @Inject + private Provider<ExceptionMappers> mappers; + + @Override + public <T> MessageBodyReader<T> getMessageBodyReader(Class<T> type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + return workers.get().getMessageBodyReader(type, genericType, annotations, mediaType); + } + + @Override + public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + return workers.get().getMessageBodyWriter(type, genericType, annotations, mediaType); + } + + @Override + public <T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> type) { + // exception mappers are not supported on the client side + final ExceptionMappers actualMappers = mappers.get(); + return (actualMappers != null) ? actualMappers.find(type) : null; + } + + @Override + public <T> ContextResolver<T> getContextResolver(Class<T> contextType, MediaType mediaType) { + return resolvers.get().resolve(contextType, mediaType); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/MapPropertiesDelegate.java b/core-common/src/main/java/org/glassfish/jersey/internal/MapPropertiesDelegate.java new file mode 100644 index 0000000..a1f8113 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/MapPropertiesDelegate.java
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Properties delegate backed by a {@code Map}. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public final class MapPropertiesDelegate implements PropertiesDelegate { + + private final Map<String, Object> store; + + /** + * Create new map-based properties delegate. + */ + public MapPropertiesDelegate() { + this.store = new HashMap<String, Object>(); + } + + /** + * Create new map-based properties delegate. + * + * @param store backing property store. + */ + public MapPropertiesDelegate(Map<String, Object> store) { + this.store = store; + } + + /** + * Initialize new map-based properties delegate from another + * delegate. + * + * @param that original properties delegate. + */ + public MapPropertiesDelegate(PropertiesDelegate that) { + if (that instanceof MapPropertiesDelegate) { + this.store = new HashMap<String, Object>(((MapPropertiesDelegate) that).store); + } else { + this.store = new HashMap<String, Object>(); + for (String name : that.getPropertyNames()) { + this.store.put(name, that.getProperty(name)); + } + } + } + + @Override + public Object getProperty(String name) { + return store.get(name); + } + + @Override + public Collection<String> getPropertyNames() { + return Collections.unmodifiableCollection(store.keySet()); + } + + @Override + public void setProperty(String name, Object value) { + store.put(name, value); + } + + @Override + public void removeProperty(String name) { + store.remove(name); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/OsgiRegistry.java b/core-common/src/main/java/org/glassfish/jersey/internal/OsgiRegistry.java new file mode 100644 index 0000000..e96fee4 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/OsgiRegistry.java
@@ -0,0 +1,603 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; + +import org.glassfish.jersey.internal.util.ReflectionHelper; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleReference; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.SynchronousBundleListener; + +/** + * Utility class to deal with OSGi runtime specific behavior. + * This is mainly to handle META-INF/services lookup + * and generic/application class lookup issue in OSGi. + * + * When OSGi runtime is detected by the {@link ServiceFinder} class, + * an instance of OsgiRegistry is created and associated with given + * OSGi BundleContext. META-INF/services entries are then being accessed + * via the OSGi Bundle API as direct ClassLoader#getResource() method invocation + * does not work in this case within OSGi. + * + * @author Jakub Podlesak (jakub.podlesak at oracle.com) + * @author Adam Lindenthal (adam.lindenthal at oracle.com) + */ +public final class OsgiRegistry implements SynchronousBundleListener { + + private static final String WEB_INF_CLASSES = "WEB-INF/classes/"; + private static final String CoreBundleSymbolicNAME = "org.glassfish.jersey.core.jersey-common"; + private static final Logger LOGGER = Logger.getLogger(OsgiRegistry.class.getName()); + + private final BundleContext bundleContext; + private final Map<Long, Map<String, Callable<List<Class<?>>>>> factories = + new HashMap<Long, Map<String, Callable<List<Class<?>>>>>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private static OsgiRegistry instance; + + private final Map<String, Bundle> classToBundleMapping = new HashMap<String, Bundle>(); + + /** + * Returns an {@code OsgiRegistry} instance. Call this method only if sure that the application is running in OSGi + * environment, otherwise a call to this method can lead to an {@link ClassNotFoundException}. + * + * @return an {@code OsgiRegistry} instance. + */ + public static synchronized OsgiRegistry getInstance() { + if (instance == null) { + final ClassLoader classLoader = AccessController + .doPrivileged(ReflectionHelper.getClassLoaderPA(ReflectionHelper.class)); + if (classLoader instanceof BundleReference) { + final BundleContext context = FrameworkUtil.getBundle(OsgiRegistry.class).getBundleContext(); + if (context != null) { // context could be still null if the current bundle has not been started + instance = new OsgiRegistry(context); + } + } + } + return instance; + } + + private final class OsgiServiceFinder extends ServiceFinder.ServiceIteratorProvider { + + final ServiceFinder.ServiceIteratorProvider defaultIterator = new ServiceFinder.DefaultServiceIteratorProvider(); + + @Override + public <T> Iterator<T> createIterator( + final Class<T> serviceClass, + final String serviceName, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) { + + final List<Class<?>> providerClasses = locateAllProviders(serviceName); + if (!providerClasses.isEmpty()) { + return new Iterator<T>() { + + Iterator<Class<?>> it = providerClasses.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @SuppressWarnings("unchecked") + @Override + public T next() { + final Class<T> nextClass = (Class<T>) it.next(); + try { + return nextClass.newInstance(); + } catch (final Exception ex) { + final ServiceConfigurationError sce = new ServiceConfigurationError(serviceName + ": " + + LocalizationMessages.PROVIDER_COULD_NOT_BE_CREATED( + nextClass.getName(), serviceClass, ex.getLocalizedMessage())); + sce.initCause(ex); + throw sce; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + return defaultIterator.createIterator(serviceClass, serviceName, loader, ignoreOnClassNotFound); + } + + @Override + public <T> Iterator<Class<T>> createClassIterator( + final Class<T> service, final String serviceName, final ClassLoader loader, final boolean ignoreOnClassNotFound) { + final List<Class<?>> providerClasses = locateAllProviders(serviceName); + if (!providerClasses.isEmpty()) { + return new Iterator<Class<T>>() { + + Iterator<Class<?>> it = providerClasses.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @SuppressWarnings("unchecked") + @Override + public Class<T> next() { + return (Class<T>) it.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + return defaultIterator.createClassIterator(service, serviceName, loader, ignoreOnClassNotFound); + } + } + + private static class BundleSpiProvidersLoader implements Callable<List<Class<?>>> { + + private final String spi; + private final URL spiRegistryUrl; + private final String spiRegistryUrlString; + private final Bundle bundle; + + BundleSpiProvidersLoader(final String spi, final URL spiRegistryUrl, final Bundle bundle) { + this.spi = spi; + this.spiRegistryUrl = spiRegistryUrl; + this.spiRegistryUrlString = spiRegistryUrl.toExternalForm(); + this.bundle = bundle; + } + + @Override + public List<Class<?>> call() throws Exception { + BufferedReader reader = null; + + try { + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, "Loading providers for SPI: {0}", spi); + } + reader = new BufferedReader(new InputStreamReader(spiRegistryUrl.openStream(), "UTF-8")); + String providerClassName; + + final List<Class<?>> providerClasses = new ArrayList<Class<?>>(); + while ((providerClassName = reader.readLine()) != null) { + if (providerClassName.trim().length() == 0) { + continue; + } + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, "SPI provider: {0}", providerClassName); + } + providerClasses.add(loadClass(bundle, providerClassName)); + } + + return providerClasses; + } catch (final Exception e) { + LOGGER.log(Level.WARNING, LocalizationMessages.EXCEPTION_CAUGHT_WHILE_LOADING_SPI_PROVIDERS(), e); + throw e; + } catch (final Error e) { + LOGGER.log(Level.WARNING, LocalizationMessages.ERROR_CAUGHT_WHILE_LOADING_SPI_PROVIDERS(), e); + throw e; + } finally { + if (reader != null) { + try { + reader.close(); + } catch (final IOException ioe) { + LOGGER.log(Level.FINE, "Error closing SPI registry stream:" + spiRegistryUrl, ioe); + } + } + } + } + + @Override + public String toString() { + return spiRegistryUrlString; + } + + @Override + public int hashCode() { + return spiRegistryUrlString.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof BundleSpiProvidersLoader) { + return spiRegistryUrlString.equals(((BundleSpiProvidersLoader) obj).spiRegistryUrlString); + } else { + return false; + } + } + } + + @Override + public void bundleChanged(final BundleEvent event) { + + if (event.getType() == BundleEvent.RESOLVED) { + register(event.getBundle()); + } else if (event.getType() == BundleEvent.UNRESOLVED || event.getType() == BundleEvent.UNINSTALLED) { + + final Bundle unregisteredBundle = event.getBundle(); + + lock.writeLock().lock(); + try { + factories.remove(unregisteredBundle.getBundleId()); + + if (unregisteredBundle.getSymbolicName().equals(CoreBundleSymbolicNAME)) { + bundleContext.removeBundleListener(this); + factories.clear(); + } + } finally { + lock.writeLock().unlock(); + } + } + } + + /** + * Translates bundle entry path as returned from {@link org.osgi.framework.Bundle#findEntries(String, String, boolean)} to + * fully qualified class name that resides in given package path (directly or indirectly in its subpackages). + * + * @param packagePath The package path where the class is located (even recursively) + * @param bundleEntryPath The bundle path to translate. + * @return Fully qualified class name. + */ + public static String bundleEntryPathToClassName(String packagePath, String bundleEntryPath) { + // normalize packagePath + packagePath = normalizedPackagePath(packagePath); + + // remove WEB-INF/classes from bundle entry path + if (bundleEntryPath.contains(WEB_INF_CLASSES)) { + bundleEntryPath = bundleEntryPath.substring(bundleEntryPath.indexOf(WEB_INF_CLASSES) + WEB_INF_CLASSES.length()); + } + + final int packageIndex = bundleEntryPath.indexOf(packagePath); + + String normalizedClassNamePath = packageIndex > -1 + // the package path was found in the bundle path + ? bundleEntryPath.substring(packageIndex) + // the package path is not included in the bundle entry path + // fall back to the original implementation of the translation which does not consider recursion + : packagePath + bundleEntryPath.substring(bundleEntryPath.lastIndexOf('/') + 1); + + return (normalizedClassNamePath.startsWith("/") ? normalizedClassNamePath.substring(1) : normalizedClassNamePath) + .replace('/', '.').replace(".class", ""); + } + + /** + * Returns whether the given entry path is located directly in the provided package path. That is, + * if the entry is located in a sub-package, then {@code false} is returned. + * + * @param packagePath Package path which the entry is compared to + * @param entryPath Entry path + * @return Whether the given entry path is located directly in the provided package path. + */ + public static boolean isPackageLevelEntry(String packagePath, final String entryPath) { + // normalize packagePath + packagePath = normalizedPackagePath(packagePath); + + // if the package path is contained in the jar entry name, subtract it + String entryWithoutPackagePath = entryPath.contains(packagePath) + ? entryPath.substring(entryPath.indexOf(packagePath) + packagePath.length()) + : entryPath; + + return !(entryWithoutPackagePath.startsWith("/") ? entryWithoutPackagePath.substring(1) + : entryWithoutPackagePath) + .contains("/"); + } + + /** + * Normalized package returns path that does not start with '/' character and ends with '/' character. + * If the argument is '/' then returned value is empty string "". + * + * @param packagePath package path to normalize. + * @return Normalized package path. + */ + public static String normalizedPackagePath(String packagePath) { + packagePath = packagePath.startsWith("/") ? packagePath.substring(1) : packagePath; + packagePath = packagePath.endsWith("/") ? packagePath : packagePath + "/"; + packagePath = "/".equals(packagePath) ? "" : packagePath; + return packagePath; + } + + /** + * Get URLs of resources from a given package. + * + * @param packagePath package. + * @param classLoader resource class loader. + * @param recursive whether the given package path should be scanned recursively by OSGi + * @return URLs of the located resources. + */ + @SuppressWarnings("unchecked") + public Enumeration<URL> getPackageResources(final String packagePath, + final ClassLoader classLoader, + final boolean recursive) { + final List<URL> result = new LinkedList<URL>(); + + for (final Bundle bundle : bundleContext.getBundles()) { + // Look for resources at the given <packagePath> and at WEB-INF/classes/<packagePath> in case a WAR is being examined. + for (final String bundlePackagePath : new String[] {packagePath, WEB_INF_CLASSES + packagePath}) { + final Enumeration<URL> enumeration = findEntries(bundle, bundlePackagePath, "*.class", recursive); + + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + final URL url = enumeration.nextElement(); + final String path = url.getPath(); + + classToBundleMapping.put(bundleEntryPathToClassName(packagePath, path), bundle); + result.add(url); + } + } + } + + // Now interested only in .jar provided by current bundle. + final Enumeration<URL> jars = findEntries(bundle, "/", "*.jar", true); + if (jars != null) { + while (jars.hasMoreElements()) { + final URL jar = jars.nextElement(); + final InputStream inputStream = classLoader.getResourceAsStream(jar.getPath()); + if (inputStream == null) { + LOGGER.config(LocalizationMessages.OSGI_REGISTRY_ERROR_OPENING_RESOURCE_STREAM(jar)); + continue; + } + final JarInputStream jarInputStream; + try { + jarInputStream = new JarInputStream(inputStream); + } catch (final IOException ex) { + LOGGER.log(Level.CONFIG, LocalizationMessages.OSGI_REGISTRY_ERROR_PROCESSING_RESOURCE_STREAM(jar), ex); + try { + inputStream.close(); + } catch (final IOException e) { + // ignored + } + continue; + } + + try { + JarEntry jarEntry; + while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { + final String jarEntryName = jarEntry.getName(); + final String jarEntryNameLeadingSlash = jarEntryName.startsWith("/") + ? jarEntryName : "/" + jarEntryName; + + if (jarEntryName.endsWith(".class") + // Added leading and trailing slashes '/' to package path (e.g. '/com/') helps us to not + // accidentally match sub-strings of the package path (e.g., if package path 'com' was used + // for scanning, package 'whatever.foo.telecom' would be matched because of word 'tele[com]'). + // Note that we cannot avoid all corner cases with accidental matches since jar + // entry name might be almost anything (e.g., if package path 'telecom' was used, package + // 'whatever.foo.telecom' will be matched and there is no way to avoid it unless user + // explicitly instructs us to do so somehow (not implemented) + && jarEntryNameLeadingSlash.contains("/" + normalizedPackagePath(packagePath))) { + if (!recursive && !isPackageLevelEntry(packagePath, jarEntryName)) { + continue; + } + classToBundleMapping.put(jarEntryName.replace(".class", "").replace('/', '.'), bundle); + result.add(bundle.getResource(jarEntryName)); + } + } + } catch (final Exception ex) { + LOGGER.log(Level.CONFIG, LocalizationMessages.OSGI_REGISTRY_ERROR_PROCESSING_RESOURCE_STREAM(jar), ex); + } finally { + try { + jarInputStream.close(); + } catch (final IOException e) { + // ignored + } + } + } + } + } + + return Collections.enumeration(result); + } + + /** + * Get the Class from the class name. + * <p> + * The context class loader will be utilized if accessible and non-null. + * Otherwise the defining class loader of this class will + * be utilized. + * + * @param className the class name. + * @return the Class, otherwise null if the class cannot be found. + * @throws ClassNotFoundException if the class cannot be found. + */ + public Class<?> classForNameWithException(final String className) throws ClassNotFoundException { + final Bundle bundle = classToBundleMapping.get(className); + + if (bundle == null) { + throw new ClassNotFoundException(className); + } + return loadClass(bundle, className); + } + + /** + * Tries to load resource bundle via OSGi means. No caching involved here, + * as localization properties are being cached in Localizer class already. + * + * @param bundleName name of the resource bundle to load + * @return resource bundle instance if found, null otherwise + */ + public ResourceBundle getResourceBundle(final String bundleName) { + final int lastDotIndex = bundleName.lastIndexOf('.'); + final String path = bundleName.substring(0, lastDotIndex).replace('.', '/'); + final String propertiesName = bundleName.substring(lastDotIndex + 1, bundleName.length()) + ".properties"; + for (final Bundle bundle : bundleContext.getBundles()) { + final Enumeration<URL> entries = findEntries(bundle, path, propertiesName, false); + if (entries != null && entries.hasMoreElements()) { + final URL entryUrl = entries.nextElement(); + try { + return new PropertyResourceBundle(entryUrl.openStream()); + } catch (final IOException ex) { + if (LOGGER.isLoggable(Level.FINE)) { + // does not make sense to localize this + LOGGER.fine("Exception caught when tried to load resource bundle in OSGi"); + } + return null; + } + } + } + return null; + } + + /** + * Creates a new OsgiRegistry instance bound to a particular OSGi runtime. + * The only parameter must be an instance of a {@link BundleContext}. + * + * @param bundleContext must be a non-null instance of a BundleContext + */ + private OsgiRegistry(final BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + /** + * Will hook up this instance with the OSGi runtime. + * This is to actually update SPI provider lookup and class loading mechanisms in Jersey + * to utilize OSGi features. + */ + void hookUp() { + setOSGiServiceFinderIteratorProvider(); + bundleContext.addBundleListener(this); + registerExistingBundles(); + } + + private void registerExistingBundles() { + for (final Bundle bundle : bundleContext.getBundles()) { + if (bundle.getState() == Bundle.RESOLVED || bundle.getState() == Bundle.STARTING + || bundle.getState() == Bundle.ACTIVE || bundle.getState() == Bundle.STOPPING) { + register(bundle); + } + } + } + + private void setOSGiServiceFinderIteratorProvider() { + ServiceFinder.setIteratorProvider(new OsgiServiceFinder()); + } + + private void register(final Bundle bundle) { + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, "checking bundle {0}", bundle.getBundleId()); + } + + Map<String, Callable<List<Class<?>>>> map; + lock.writeLock().lock(); + try { + map = factories.get(bundle.getBundleId()); + if (map == null) { + map = new ConcurrentHashMap<String, Callable<List<Class<?>>>>(); + factories.put(bundle.getBundleId(), map); + } + } finally { + lock.writeLock().unlock(); + } + + final Enumeration<URL> e = findEntries(bundle, "META-INF/services/", "*", false); + if (e != null) { + while (e.hasMoreElements()) { + final URL u = e.nextElement(); + final String url = u.toString(); + if (url.endsWith("/")) { + continue; + } + final String factoryId = url.substring(url.lastIndexOf("/") + 1); + map.put(factoryId, new BundleSpiProvidersLoader(factoryId, u, bundle)); + } + } + } + + private List<Class<?>> locateAllProviders(final String serviceName) { + lock.readLock().lock(); + try { + final List<Class<?>> result = new LinkedList<Class<?>>(); + for (final Map<String, Callable<List<Class<?>>>> value : factories.values()) { + if (value.containsKey(serviceName)) { + try { + result.addAll(value.get(serviceName).call()); + } catch (final Exception ex) { + // ignore + } + } + } + return result; + } finally { + lock.readLock().unlock(); + } + } + + private static Class<?> loadClass(final Bundle bundle, final String className) throws ClassNotFoundException { + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() { + @Override + public Class<?> run() throws ClassNotFoundException { + return bundle.loadClass(className); + } + }); + } catch (final PrivilegedActionException ex) { + final Exception originalException = ex.getException(); + if (originalException instanceof ClassNotFoundException) { + throw (ClassNotFoundException) originalException; + } else if (originalException instanceof RuntimeException) { + throw (RuntimeException) originalException; + } else { + throw new ProcessingException(originalException); + } + } + } + + private static Enumeration<URL> findEntries(final Bundle bundle, + final String path, + final String fileNamePattern, + final boolean recursive) { + return AccessController.doPrivileged(new PrivilegedAction<Enumeration<URL>>() { + @SuppressWarnings("unchecked") + @Override + public Enumeration<URL> run() { + return bundle.findEntries(path, fileNamePattern, recursive); + } + }); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/PropertiesDelegate.java b/core-common/src/main/java/org/glassfish/jersey/internal/PropertiesDelegate.java new file mode 100644 index 0000000..d0cde50 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/PropertiesDelegate.java
@@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.util.Collection; + +/** + * TODO: javadoc. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public interface PropertiesDelegate { + /** + * Returns the property with the given name registered in the current request/response + * exchange context, or {@code null} if there is no property by that name. + * <p> + * A property allows a JAX-RS filters and interceptors to exchange + * additional custom information not already provided by this interface. + * </p> + * <p> + * A list of supported properties can be retrieved using {@link #getPropertyNames()}. + * Custom property names should follow the same convention as package names. + * </p> + * + * @param name a {@code String} specifying the name of the property. + * @return an {@code Object} containing the value of the property, or + * {@code null} if no property exists matching the given name. + * @see #getPropertyNames() + */ + public Object getProperty(String name); + + + /** + * Returns an immutable {@link java.util.Collection collection} containing the property + * names available within the context of the current request/response exchange context. + * <p> + * Use the {@link #getProperty} method with a property name to get the value of + * a property. + * </p> + * + * @return an immutable {@link java.util.Collection collection} of property names. + * @see #getProperty + */ + public Collection<String> getPropertyNames(); + + + /** + * Binds an object to a given property name in the current request/response + * exchange context. If the name specified is already used for a property, + * this method will replace the value of the property with the new value. + * <p> + * A property allows a JAX-RS filters and interceptors to exchange + * additional custom information not already provided by this interface. + * </p> + * <p> + * A list of supported properties can be retrieved using {@link #getPropertyNames()}. + * Custom property names should follow the same convention as package names. + * </p> + * <p> + * If a {@code null} value is passed, the effect is the same as calling the + * {@link #removeProperty(String)} method. + * </p> + * + * @param name a {@code String} specifying the name of the property. + * @param object an {@code Object} representing the property to be bound. + */ + public void setProperty(String name, Object object); + + /** + * Removes a property with the given name from the current request/response + * exchange context. After removal, subsequent calls to {@link #getProperty} + * to retrieve the property value will return {@code null}. + * + * @param name a {@code String} specifying the name of the property to be removed. + */ + public void removeProperty(String name); +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java b/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java new file mode 100644 index 0000000..7fb25a6 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/RuntimeDelegateImpl.java
@@ -0,0 +1,46 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import javax.ws.rs.core.Application; + +import org.glassfish.jersey.message.internal.MessagingBinders; + +/** + * Default implementation of JAX-RS {@link javax.ws.rs.ext.RuntimeDelegate}. + * The {@link javax.ws.rs.ext.RuntimeDelegate} class looks for the implementations registered + * in META-INF/services. If no such implementation is found, this one is picked + * as the default. Server injection binder should override this (using META-INF/services) + * to provide an implementation that supports {@link #createEndpoint(javax.ws.rs.core.Application, java.lang.Class)} + * method. + * + * @author Jakub Podlesak + * @author Marek Potociar (marek.potociar at oracle.com) + * @author Martin Matula + */ +public class RuntimeDelegateImpl extends AbstractRuntimeDelegate { + + public RuntimeDelegateImpl() { + super(new MessagingBinders.HeaderDelegateProviders().getHeaderDelegateProviders()); + } + + @Override + public <T> T createEndpoint(Application application, Class<T> endpointType) + throws IllegalArgumentException, UnsupportedOperationException { + throw new UnsupportedOperationException(LocalizationMessages.NO_CONTAINER_AVAILABLE()); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/ServiceConfigurationError.java b/core-common/src/main/java/org/glassfish/jersey/internal/ServiceConfigurationError.java new file mode 100644 index 0000000..fa3c542 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/ServiceConfigurationError.java
@@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +/** + * Error thrown when something goes wrong while looking up service providers. + * In particular, this error will be thrown in the following situations: + * + * <ul> + * <li> A concrete provider class cannot be found, + * <li> A concrete provider class cannot be instantiated, + * <li> The format of a provider-configuration file is illegal, or + * <li> An IOException occurs while reading a provider-configuration file. + * </ul> + * + * @author Mark Reinhold + * @author Marek Potociar (marek.potociar at oracle.com) + */ +public class ServiceConfigurationError extends Error { + + private static final long serialVersionUID = -8532392338326428074L; + + /** + * Constructs a new instance with the specified detail string. + * @param msg the detail string + */ + public ServiceConfigurationError(String msg) { + super(msg); + } + + /** + * Constructs a new instance that wraps the specified throwable. + * @param x the throwable to be wrapped + */ + public ServiceConfigurationError(Throwable x) { + super(x); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/ServiceFinder.java b/core-common/src/main/java/org/glassfish/jersey/internal/ServiceFinder.java new file mode 100644 index 0000000..46184d8 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/ServiceFinder.java
@@ -0,0 +1,883 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Array; +import java.lang.reflect.ReflectPermission; +import java.net.URL; +import java.net.URLConnection; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.glassfish.jersey.internal.util.ReflectionHelper; + +/** + * A simple service-provider lookup mechanism. A <i>service</i> is a + * well-known set of interfaces and (usually abstract) classes. A <i>service + * provider</i> is a specific implementation of a service. The classes in a + * provider typically implement the interfaces and subclass the classes defined + * in the service itself. Service providers may be installed in an + * implementation of the Java platform in the form of extensions, that is, jar + * files placed into any of the usual extension directories. Providers may + * also be made available by adding them to the applet or application class + * path or by some other platform-specific means. + * <p/> + * <p> In this lookup mechanism a service is represented by an interface or an + * abstract class. (A concrete class may be used, but this is not + * recommended.) A provider of a given service contains one or more concrete + * classes that extend this <i>service class</i> with data and code specific to + * the provider. This <i>provider class</i> will typically not be the entire + * provider itself but rather a proxy that contains enough information to + * decide whether the provider is able to satisfy a particular request together + * with code that can create the actual provider on demand. The details of + * provider classes tend to be highly service-specific; no single class or + * interface could possibly unify them, so no such class has been defined. The + * only requirement enforced here is that provider classes must have a + * zero-argument constructor so that they may be instantiated during lookup. + * <p/> + * <p>The default service provider registration/lookup mechanism based + * on <tt>META-INF/services</tt> files is described below. + * For environments, where the basic mechanism is not suitable, clients + * can enforce a different approach by setting their custom <tt>ServiceIteratorProvider</tt> + * by calling <tt>setIteratorProvider</tt>. The call must be made prior to any lookup attempts. + * </p> + * <p> A service provider identifies itself by placing a provider-configuration + * file in the resource directory <tt>META-INF/services</tt>. The file's name + * should consist of the fully-qualified name of the abstract service class. + * The file should contain a list of fully-qualified concrete provider-class + * names, one per line. Space and tab characters surrounding each name, as + * well as blank lines, are ignored. The comment character is <tt>'#'</tt> + * (<tt>0x23</tt>); on each line all characters following the first comment + * character are ignored. The file must be encoded in UTF-8. + * <p/> + * <p> If a particular concrete provider class is named in more than one + * configuration file, or is named in the same configuration file more than + * once, then the duplicates will be ignored. The configuration file naming a + * particular provider need not be in the same jar file or other distribution + * unit as the provider itself. The provider must be accessible from the same + * class loader that was initially queried to locate the configuration file; + * note that this is not necessarily the class loader that found the file. + * <p/> + * <p> <b>Example:</b> Suppose we have a service class named + * <tt>java.io.spi.CharCodec</tt>. It has two abstract methods: + * <p/> + * <pre> + * public abstract CharEncoder getEncoder(String encodingName); + * public abstract CharDecoder getDecoder(String encodingName); + * </pre> + * <p/> + * Each method returns an appropriate object or <tt>null</tt> if it cannot + * translate the given encoding. Typical <tt>CharCodec</tt> providers will + * support more than one encoding. + * <p/> + * <p> If <tt>sun.io.StandardCodec</tt> is a provider of the <tt>CharCodec</tt> + * service then its jar file would contain the file + * <tt>META-INF/services/java.io.spi.CharCodec</tt>. This file would contain + * the single line: + * <p/> + * <pre> + * sun.io.StandardCodec # Standard codecs for the platform + * </pre> + * <p/> + * To locate an codec for a given encoding name, the internal I/O code would + * do something like this: + * <p/> + * <pre> + * CharEncoder getEncoder(String encodingName) { + * for( CharCodec cc : ServiceFinder.find(CharCodec.class) ) { + * CharEncoder ce = cc.getEncoder(encodingName); + * if (ce != null) + * return ce; + * } + * return null; + * } + * </pre> + * <p/> + * The provider-lookup mechanism always executes in the security context of the + * caller. Trusted system code should typically invoke the methods in this + * class from within a privileged security context. + * + * @param <T> the type of the service instance. + * @author Mark Reinhold + * @author Jakub Podlesak + * @author Marek Potociar + */ +public final class ServiceFinder<T> implements Iterable<T> { + + private static final Logger LOGGER = Logger.getLogger(ServiceFinder.class.getName()); + private static final String PREFIX = "META-INF/services/"; + private final Class<T> serviceClass; + private final String serviceName; + private final ClassLoader classLoader; + private final boolean ignoreOnClassNotFound; + + static { + final OsgiRegistry osgiRegistry = ReflectionHelper.getOsgiRegistryInstance(); + + if (osgiRegistry != null) { + LOGGER.log(Level.CONFIG, "Running in an OSGi environment"); + + osgiRegistry.hookUp(); + } else { + LOGGER.log(Level.CONFIG, "Running in a non-OSGi environment"); + } + } + + private static Enumeration<URL> getResources(final ClassLoader loader, final String name) throws IOException { + if (loader == null) { + return getResources(name); + } else { + final Enumeration<URL> resources = loader.getResources(name); + if ((resources != null) && resources.hasMoreElements()) { + return resources; + } else { + return getResources(name); + } + } + } + + private static Enumeration<URL> getResources(final String name) throws IOException { + if (ServiceFinder.class.getClassLoader() != null) { + return ServiceFinder.class.getClassLoader().getResources(name); + } else { + return ClassLoader.getSystemResources(name); + } + } + + private static ClassLoader _getContextClassLoader() { + return AccessController.doPrivileged(ReflectionHelper.getContextClassLoaderPA()); + } + + /** + * Locates and incrementally instantiates the available providers of a + * given service using the given class loader. + * <p/> + * <p> This method transforms the name of the given service class into a + * provider-configuration filename as described above and then uses the + * <tt>getResources</tt> method of the given class loader to find all + * available files with that name. These files are then read and parsed to + * produce a list of provider-class names. The iterator that is returned + * uses the given class loader to lookup and then instantiate each element + * of the list. + * <p/> + * <p> Because it is possible for extensions to be installed into a running + * Java virtual machine, this method may return different results each time + * it is invoked. <p> + * @param service The service's abstract service class + * @param loader The class loader to be used to load provider-configuration files + * and instantiate provider classes, or <tt>null</tt> if the system + * class loader (or, failing that the bootstrap class loader) is to + * be used + * @throws ServiceConfigurationError If a provider-configuration file violates the specified format + * or names a provider class that cannot be found and instantiated + * @see #find(Class) + * @param <T> the type of the service instance. + * @return the service finder + */ + public static <T> ServiceFinder<T> find(final Class<T> service, final ClassLoader loader) + throws ServiceConfigurationError { + return find(service, + loader, + false); + } + + /** + * Locates and incrementally instantiates the available providers of a + * given service using the given class loader. + * <p/> + * <p> This method transforms the name of the given service class into a + * provider-configuration filename as described above and then uses the + * <tt>getResources</tt> method of the given class loader to find all + * available files with that name. These files are then read and parsed to + * produce a list of provider-class names. The iterator that is returned + * uses the given class loader to lookup and then instantiate each element + * of the list. + * <p/> + * <p> Because it is possible for extensions to be installed into a running + * Java virtual machine, this method may return different results each time + * it is invoked. <p> + * @param service The service's abstract service class + * @param loader The class loader to be used to load provider-configuration files + * and instantiate provider classes, or <tt>null</tt> if the system + * class loader (or, failing that the bootstrap class loader) is to + * be used + * @param ignoreOnClassNotFound If a provider cannot be loaded by the class loader + * then move on to the next available provider. + * @throws ServiceConfigurationError If a provider-configuration file violates the specified format + * or names a provider class that cannot be found and instantiated + * @see #find(Class) + * @param <T> the type of the service instance. + * @return the service finder + */ + public static <T> ServiceFinder<T> find(final Class<T> service, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) throws ServiceConfigurationError { + return new ServiceFinder<T>(service, + loader, + ignoreOnClassNotFound); + } + + /** + * Locates and incrementally instantiates the available providers of a + * given service using the context class loader. This convenience method + * is equivalent to + * <p/> + * <pre> + * ClassLoader cl = Thread.currentThread().getContextClassLoader(); + * return Service.providers(service, cl, false); + * </pre> + * @param service The service's abstract service class + * @throws ServiceConfigurationError If a provider-configuration file violates the specified format + * or names a provider class that cannot be found and instantiated + * @see #find(Class, ClassLoader) + * @param <T> the type of the service instance. + * @return the service finder + */ + public static <T> ServiceFinder<T> find(final Class<T> service) + throws ServiceConfigurationError { + return find(service, + _getContextClassLoader(), + false); + } + + /** + * Locates and incrementally instantiates the available providers of a + * given service using the context class loader. This convenience method + * is equivalent to + * <p/> + * <pre> + * ClassLoader cl = Thread.currentThread().getContextClassLoader(); + * boolean ingore = ... + * return Service.providers(service, cl, ignore); + * </pre> + * @param service The service's abstract service class + * @param ignoreOnClassNotFound If a provider cannot be loaded by the class loader + * then move on to the next available provider. + * @throws ServiceConfigurationError If a provider-configuration file violates the specified format + * or names a provider class that cannot be found and instantiated + * @see #find(Class, ClassLoader) + * @param <T> the type of the service instance. + * @return the service finder + */ + public static <T> ServiceFinder<T> find(final Class<T> service, + final boolean ignoreOnClassNotFound) throws ServiceConfigurationError { + return find(service, + _getContextClassLoader(), + ignoreOnClassNotFound); + } + + /** + * Locates and incrementally instantiates the available classes of a given + * service file using the context class loader. + * + * @param serviceName the service name correspond to a file in + * META-INF/services that contains a list of fully qualified class + * names + * @throws ServiceConfigurationError If a service file violates the specified format + * or names a provider class that cannot be found and instantiated + * @return the service finder + */ + public static ServiceFinder<?> find(final String serviceName) throws ServiceConfigurationError { + return new ServiceFinder<Object>(Object.class, serviceName, _getContextClassLoader(), false); + } + + /** + * Register the service iterator provider to iterate on provider instances + * or classes. + * <p> + * The default implementation registered, {@link DefaultServiceIteratorProvider}, + * looks up provider classes in META-INF/service files. + * <p> + * This method must be called prior to any attempts to obtain provider + * instances or classes. + * + * @param sip the service iterator provider. + * @throws SecurityException if the provider cannot be registered. + */ + public static void setIteratorProvider(final ServiceIteratorProvider sip) throws SecurityException { + ServiceIteratorProvider.setInstance(sip); + } + + private ServiceFinder( + final Class<T> service, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) { + this(service, service.getName(), loader, ignoreOnClassNotFound); + } + + private ServiceFinder( + final Class<T> service, + final String serviceName, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) { + this.serviceClass = service; + this.serviceName = serviceName; + this.classLoader = loader; + this.ignoreOnClassNotFound = ignoreOnClassNotFound; + } + + /** + * Returns discovered objects incrementally. + * + * @return An <tt>Iterator</tt> that yields provider objects for the given + * service, in some arbitrary order. The iterator will throw a + * <tt>ServiceConfigurationError</tt> if a provider-configuration + * file violates the specified format or if a provider class cannot + * be found and instantiated. + */ + @Override + public Iterator<T> iterator() { + return ServiceIteratorProvider.getInstance() + .createIterator(serviceClass, serviceName, classLoader, ignoreOnClassNotFound); + } + + /** + * Returns discovered objects all at once. + * + * @return + * can be empty but never null. + * + * @throws ServiceConfigurationError If a provider-configuration file violates the specified format + * or names a provider class that cannot be found and instantiated + */ + @SuppressWarnings("unchecked") + public T[] toArray() throws ServiceConfigurationError { + final List<T> result = new ArrayList<T>(); + for (final T t : this) { + result.add(t); + } + return result.toArray((T[]) Array.newInstance(serviceClass, result.size())); + } + + /** + * Returns discovered classes all at once. + * + * @return + * can be empty but never null. + * + * @throws ServiceConfigurationError If a provider-configuration file violates the specified format + * or names a provider class that cannot be found + */ + @SuppressWarnings("unchecked") + public Class<T>[] toClassArray() throws ServiceConfigurationError { + final List<Class<T>> result = new ArrayList<Class<T>>(); + + final ServiceIteratorProvider iteratorProvider = ServiceIteratorProvider.getInstance(); + final Iterator<Class<T>> i = iteratorProvider + .createClassIterator(serviceClass, serviceName, classLoader, ignoreOnClassNotFound); + while (i.hasNext()) { + result.add(i.next()); + } + return result.toArray((Class<T>[]) Array.newInstance(Class.class, result.size())); + } + + private static void fail(final String serviceName, final String msg, final Throwable cause) + throws ServiceConfigurationError { + final ServiceConfigurationError sce = new ServiceConfigurationError(serviceName + ": " + msg); + sce.initCause(cause); + throw sce; + } + + private static void fail(final String serviceName, final String msg) + throws ServiceConfigurationError { + throw new ServiceConfigurationError(serviceName + ": " + msg); + } + + private static void fail(final String serviceName, final URL u, final int line, final String msg) + throws ServiceConfigurationError { + fail(serviceName, u + ":" + line + ": " + msg); + } + + /** + * Parse a single line from the given configuration file, adding the name + * on the line to both the names list and the returned set iff the name is + * not already a member of the returned set. + */ + private static int parseLine(final String serviceName, final URL u, final BufferedReader r, final int lc, + final List<String> names, final Set<String> returned) + throws IOException, ServiceConfigurationError { + String ln = r.readLine(); + if (ln == null) { + return -1; + } + final int ci = ln.indexOf('#'); + if (ci >= 0) { + ln = ln.substring(0, ci); + } + ln = ln.trim(); + final int n = ln.length(); + if (n != 0) { + if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) { + fail(serviceName, u, lc, LocalizationMessages.ILLEGAL_CONFIG_SYNTAX()); + } + int cp = ln.codePointAt(0); + if (!Character.isJavaIdentifierStart(cp)) { + fail(serviceName, u, lc, LocalizationMessages.ILLEGAL_PROVIDER_CLASS_NAME(ln)); + } + for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { + cp = ln.codePointAt(i); + if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) { + fail(serviceName, u, lc, LocalizationMessages.ILLEGAL_PROVIDER_CLASS_NAME(ln)); + } + } + if (!returned.contains(ln)) { + names.add(ln); + returned.add(ln); + } + } + return lc + 1; + } + + /** + * Parse the content of the given URL as a provider-configuration file. + * + * @param serviceName The service class for which providers are being sought; + * used to construct error detail strings + * @param u The URL naming the configuration file to be parsed + * @param returned A Set containing the names of provider classes that have already + * been returned. This set will be updated to contain the names + * that will be yielded from the returned <tt>Iterator</tt>. + * @return A (possibly empty) <tt>Iterator</tt> that will yield the + * provider-class names in the given configuration file that are + * not yet members of the returned set + * @throws ServiceConfigurationError If an I/O error occurs while reading from the given URL, or + * if a configuration-file format error is detected + */ + @SuppressWarnings({"StatementWithEmptyBody"}) + private static Iterator<String> parse(final String serviceName, final URL u, final Set<String> returned) + throws ServiceConfigurationError { + InputStream in = null; + BufferedReader r = null; + final ArrayList<String> names = new ArrayList<String>(); + try { + final URLConnection uConn = u.openConnection(); + uConn.setUseCaches(false); + in = uConn.getInputStream(); + r = new BufferedReader(new InputStreamReader(in, "utf-8")); + int lc = 1; + while ((lc = parseLine(serviceName, u, r, lc, names, returned)) >= 0) { + // continue + } + } catch (final IOException x) { + fail(serviceName, ": " + x); + } finally { + try { + if (r != null) { + r.close(); + } + if (in != null) { + in.close(); + } + } catch (final IOException y) { + fail(serviceName, ": " + y); + } + } + return names.iterator(); + } + + private static class AbstractLazyIterator<T> { + + final Class<T> service; + final String serviceName; + final ClassLoader loader; + final boolean ignoreOnClassNotFound; + Enumeration<URL> configs = null; + Iterator<String> pending = null; + Set<String> returned = new TreeSet<String>(); + String nextName = null; + + private AbstractLazyIterator( + final Class<T> service, + final String serviceName, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) { + this.service = service; + this.serviceName = serviceName; + this.loader = loader; + this.ignoreOnClassNotFound = ignoreOnClassNotFound; + } + + protected final void setConfigs() { + if (configs == null) { + try { + final String fullName = PREFIX + serviceName; + configs = getResources(loader, fullName); + } catch (final IOException x) { + fail(serviceName, ": " + x); + } + } + } + + public boolean hasNext() throws ServiceConfigurationError { + if (nextName != null) { + return true; + } + setConfigs(); + + while (nextName == null) { + while ((pending == null) || !pending.hasNext()) { + if (!configs.hasMoreElements()) { + return false; + } + pending = parse(serviceName, configs.nextElement(), returned); + } + nextName = pending.next(); + if (ignoreOnClassNotFound) { + try { + AccessController.doPrivileged(ReflectionHelper.classForNameWithExceptionPEA(nextName, loader)); + } catch (final ClassNotFoundException ex) { + handleClassNotFoundException(); + } catch (final PrivilegedActionException pae) { + final Throwable thrown = pae.getException(); + if (thrown instanceof ClassNotFoundException) { + handleClassNotFoundException(); + } else if (thrown instanceof NoClassDefFoundError) { + // Dependent class of provider not found + if (LOGGER.isLoggable(Level.CONFIG)) { + // This assumes that ex.getLocalizedMessage() returns + // the name of a dependent class that is not found + LOGGER.log(Level.CONFIG, + LocalizationMessages.DEPENDENT_CLASS_OF_PROVIDER_NOT_FOUND( + thrown.getLocalizedMessage(), nextName, service)); + } + nextName = null; + } else if (thrown instanceof ClassFormatError) { + // Dependent class of provider not found + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, + LocalizationMessages.DEPENDENT_CLASS_OF_PROVIDER_FORMAT_ERROR( + thrown.getLocalizedMessage(), nextName, service)); + } + nextName = null; + } else if (thrown instanceof RuntimeException) { + throw (RuntimeException) thrown; + } else { + throw new IllegalStateException(thrown); + } + } + } + } + return true; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + private void handleClassNotFoundException() { + // Provider implementation not found + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, + LocalizationMessages.PROVIDER_NOT_FOUND(nextName, service)); + } + nextName = null; + } + } + + private static final class LazyClassIterator<T> extends AbstractLazyIterator<T> + implements Iterator<Class<T>> { + + private LazyClassIterator( + final Class<T> service, + final String serviceName, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) { + super(service, serviceName, loader, ignoreOnClassNotFound); + } + + @Override + public Class<T> next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + final String cn = nextName; + nextName = null; + try { + + final Class<T> tClass = AccessController.doPrivileged( + ReflectionHelper.<T>classForNameWithExceptionPEA(cn, loader)); + + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, "Loading next class: " + tClass.getName()); + } + + return tClass; + + } catch (final ClassNotFoundException ex) { + fail(serviceName, + LocalizationMessages.PROVIDER_NOT_FOUND(cn, service)); + } catch (final PrivilegedActionException pae) { + + final Throwable thrown = pae.getCause(); + + if (thrown instanceof ClassNotFoundException) { + fail(serviceName, + LocalizationMessages.PROVIDER_NOT_FOUND(cn, service)); + } else if (thrown instanceof NoClassDefFoundError) { + fail(serviceName, + LocalizationMessages.DEPENDENT_CLASS_OF_PROVIDER_NOT_FOUND( + thrown.getLocalizedMessage(), cn, service)); + } else if (thrown instanceof ClassFormatError) { + fail(serviceName, + LocalizationMessages.DEPENDENT_CLASS_OF_PROVIDER_FORMAT_ERROR( + thrown.getLocalizedMessage(), cn, service)); + } else { + fail(serviceName, + LocalizationMessages.PROVIDER_CLASS_COULD_NOT_BE_LOADED(cn, service, thrown.getLocalizedMessage()), + thrown); + } + } + + return null; /* This cannot happen */ + } + } + + private static final class LazyObjectIterator<T> extends AbstractLazyIterator<T> implements Iterator<T> { + + private T t; + + private LazyObjectIterator( + final Class<T> service, + final String serviceName, + final ClassLoader loader, + final boolean ignoreOnClassNotFound) { + super(service, serviceName, loader, ignoreOnClassNotFound); + } + + @Override + public boolean hasNext() throws ServiceConfigurationError { + if (nextName != null) { + return true; + } + setConfigs(); + + while (nextName == null) { + while ((pending == null) || !pending.hasNext()) { + if (!configs.hasMoreElements()) { + return false; + } + pending = parse(serviceName, configs.nextElement(), returned); + } + nextName = pending.next(); + try { + t = service.cast(AccessController.doPrivileged( + ReflectionHelper.classForNameWithExceptionPEA(nextName, loader)).newInstance()); + + } catch (final InstantiationException ex) { + if (ignoreOnClassNotFound) { + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, + LocalizationMessages.PROVIDER_COULD_NOT_BE_CREATED(nextName, service, + ex.getLocalizedMessage())); + } + nextName = null; + } else { + fail(serviceName, + LocalizationMessages.PROVIDER_COULD_NOT_BE_CREATED(nextName, service, ex.getLocalizedMessage()), + ex); + } + } catch (final IllegalAccessException ex) { + fail(serviceName, + LocalizationMessages.PROVIDER_COULD_NOT_BE_CREATED(nextName, service, ex.getLocalizedMessage()), + ex); + + } catch (final ClassNotFoundException ex) { + handleClassNotFoundException(); + } catch (final NoClassDefFoundError ex) { + // Dependent class of provider not found + if (ignoreOnClassNotFound) { + if (LOGGER.isLoggable(Level.CONFIG)) { + // This assumes that ex.getLocalizedMessage() returns + // the name of a dependent class that is not found + LOGGER.log(Level.CONFIG, + LocalizationMessages.DEPENDENT_CLASS_OF_PROVIDER_NOT_FOUND( + ex.getLocalizedMessage(), nextName, service)); + } + nextName = null; + } else { + fail(serviceName, + LocalizationMessages + .DEPENDENT_CLASS_OF_PROVIDER_NOT_FOUND(ex.getLocalizedMessage(), nextName, service), + ex); + } + + } catch (final PrivilegedActionException pae) { + final Throwable cause = pae.getCause(); + if (cause instanceof ClassNotFoundException) { + handleClassNotFoundException(); + } else if (cause instanceof ClassFormatError) { + // Dependent class of provider not found + if (ignoreOnClassNotFound) { + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, + LocalizationMessages.DEPENDENT_CLASS_OF_PROVIDER_FORMAT_ERROR( + cause.getLocalizedMessage(), nextName, service)); + } + nextName = null; + } else { + fail(serviceName, + LocalizationMessages + .DEPENDENT_CLASS_OF_PROVIDER_FORMAT_ERROR(cause.getLocalizedMessage(), nextName, + service), + cause); + } + } else { + fail(serviceName, + LocalizationMessages + .PROVIDER_COULD_NOT_BE_CREATED(nextName, service, cause.getLocalizedMessage()), + cause); + } + } + } + return true; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + nextName = null; + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, "Loading next object: " + t.getClass().getName()); + } + return t; + } + + private void handleClassNotFoundException() throws ServiceConfigurationError { + if (ignoreOnClassNotFound) { + // Provider implementation not found + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, + LocalizationMessages.PROVIDER_NOT_FOUND(nextName, service)); + } + nextName = null; + } else { + fail(serviceName, + LocalizationMessages.PROVIDER_NOT_FOUND(nextName, service)); + } + } + } + + /** + * Supports iteration of provider instances or classes. + * <p> + * The default implementation looks up provider classes from META-INF/services + * files, see {@link DefaultServiceIteratorProvider}. + * This implementation may be overridden by invoking + * {@link ServiceFinder#setIteratorProvider(org.glassfish.jersey.internal.ServiceFinder.ServiceIteratorProvider)}. + */ + public abstract static class ServiceIteratorProvider { + + private static volatile ServiceIteratorProvider sip; + private static final Object sipLock = new Object(); + + private static ServiceIteratorProvider getInstance() { + // TODO: check the following is a good practice: Double-check idiom for lazy initialization of fields. + ServiceIteratorProvider result = sip; + if (result == null) { // First check (no locking) + synchronized (sipLock) { + result = sip; + if (result == null) { // Second check (with locking) + sip = result = new DefaultServiceIteratorProvider(); + } + } + } + return result; + } + + private static void setInstance(final ServiceIteratorProvider sip) throws SecurityException { + final SecurityManager security = System.getSecurityManager(); + if (security != null) { + final ReflectPermission rp = new ReflectPermission("suppressAccessChecks"); + security.checkPermission(rp); + } + synchronized (sipLock) { + ServiceIteratorProvider.sip = sip; + } + } + + /** + * Iterate over provider instances of a service. + * + * @param <T> the type of the service. + * @param service the service class. + * @param serviceName the service name. + * @param loader the class loader to utilize when loading provider + * classes. + * @param ignoreOnClassNotFound if true ignore an instance if the + * corresponding provider class if cannot be found, + * otherwise throw a {@link ClassNotFoundException}. + * @return the provider instance iterator. + */ + public abstract <T> Iterator<T> createIterator(Class<T> service, + String serviceName, ClassLoader loader, boolean ignoreOnClassNotFound); + + /** + * Iterate over provider classes of a service. + * + * @param <T> the type of the service. + * @param service the service class. + * @param serviceName the service name. + * @param loader the class loader to utilize when loading provider + * classes. + * @param ignoreOnClassNotFound if true ignore the provider class if + * cannot be found, + * otherwise throw a {@link ClassNotFoundException}. + * @return the provider class iterator. + */ + public abstract <T> Iterator<Class<T>> createClassIterator(Class<T> service, + String serviceName, + ClassLoader loader, + boolean ignoreOnClassNotFound); + } + + /** + * The default service iterator provider that looks up provider classes in + * META-INF/services files. + * <p> + * This class may utilized if a {@link ServiceIteratorProvider} needs to + * reuse the default implementation. + */ + public static final class DefaultServiceIteratorProvider extends ServiceIteratorProvider { + + @Override + public <T> Iterator<T> createIterator(final Class<T> service, final String serviceName, + final ClassLoader loader, final boolean ignoreOnClassNotFound) { + return new LazyObjectIterator<T>(service, serviceName, loader, ignoreOnClassNotFound); + } + + @Override + public <T> Iterator<Class<T>> createClassIterator(final Class<T> service, final String serviceName, + final ClassLoader loader, final boolean ignoreOnClassNotFound) { + return new LazyClassIterator<T>(service, serviceName, loader, ignoreOnClassNotFound); + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/ServiceFinderBinder.java b/core-common/src/main/java/org/glassfish/jersey/internal/ServiceFinderBinder.java new file mode 100644 index 0000000..cc0da5e --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/ServiceFinderBinder.java
@@ -0,0 +1,72 @@ +/* + * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.util.Map; + +import javax.ws.rs.RuntimeType; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.InjectionManager; + +/** + * Simple ServiceFinder injection binder. + * + * Looks for all implementations of a given contract using {@link ServiceFinder} + * and registers found instances to {@link InjectionManager}. + * + * @param <T> contract type. + * @author Pavel Bucek (pavel.bucek at oracle.com) + * @author Libor Kramolis (libor.kramolis at oracle.com) + */ +public class ServiceFinderBinder<T> extends AbstractBinder { + + private final Class<T> contract; + + private final Map<String, Object> applicationProperties; + + private final RuntimeType runtimeType; + + /** + * Create a new service finder injection binder. + * + * @param contract contract of the service providers bound by this binder. + * @param applicationProperties map containing application properties. May be {@code null}. + * @param runtimeType runtime (client or server) where the service finder binder is used. + */ + public ServiceFinderBinder(Class<T> contract, Map<String, Object> applicationProperties, RuntimeType runtimeType) { + this.contract = contract; + this.applicationProperties = applicationProperties; + this.runtimeType = runtimeType; + } + + @Override + protected void configure() { + final boolean METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT = false; + boolean disableMetainfServicesLookup = METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT; + if (applicationProperties != null) { + disableMetainfServicesLookup = CommonProperties.getValue(applicationProperties, runtimeType, + CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, METAINF_SERVICES_LOOKUP_DISABLE_DEFAULT, Boolean.class); + } + if (!disableMetainfServicesLookup) { + for (Class<T> t : ServiceFinder.find(contract, true).toClassArray()) { + bind(t).to(contract); + } + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/Version.java b/core-common/src/main/java/org/glassfish/jersey/internal/Version.java new file mode 100644 index 0000000..9afd64a --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/Version.java
@@ -0,0 +1,91 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal; + +import java.io.InputStream; +import java.util.Properties; + +/** + * Utility class for reading build.properties file. + * + * @author Paul Sandoz + */ +public final class Version { + + private static String buildId; + private static String version = null; + + static { + _initiateProperties(); + } + + private Version() { + throw new AssertionError("Instantiation not allowed."); + } + + private static void _initiateProperties() { + final InputStream in = getIntputStream(); + if (in != null) { + try { + final Properties p = new Properties(); + p.load(in); + final String timestamp = p.getProperty("Build-Timestamp"); + version = p.getProperty("Build-Version"); + + buildId = String.format("Jersey: %s %s", version, timestamp); + } catch (final Exception e) { + buildId = "Jersey"; + } finally { + close(in); + } + } + } + + private static void close(final InputStream in) { + try { + in.close(); + } catch (final Exception ex) { + // Ignore + } + } + + private static InputStream getIntputStream() { + try { + return Version.class.getResourceAsStream("build.properties"); + } catch (final Exception ex) { + return null; + } + } + + /** + * Get build id. + * + * @return build id string. Contains version and build timestamp. + */ + public static String getBuildId() { + return buildId; + } + + /** + * Get Jersey version. + * + * @return Jersey version. + */ + public static String getVersion() { + return version; + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractFuture.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractFuture.java new file mode 100644 index 0000000..9da9704 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractFuture.java
@@ -0,0 +1,397 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; + +/** + * An abstract implementation of the {@link ListenableFuture} interface. This + * class is preferable to {@link java.util.concurrent.FutureTask} for two + * reasons: It implements {@code ListenableFuture}, and it does not implement + * {@code Runnable}. (If you want a {@code Runnable} implementation of {@code + * ListenableFuture}, create a {@link ListenableFutureTask}, or submit your + * tasks to a {@link ListeningExecutorService}.) + * <p> + * <p>This class implements all methods in {@code ListenableFuture}. + * Subclasses should provide a way to set the result of the computation through + * the protected methods {@link #set(Object)} and + * {@link #setException(Throwable)}. Subclasses may also override {@link + * #interruptTask()}, which will be invoked automatically if a call to {@link + * #cancel(boolean) cancel(true)} succeeds in canceling the future. + * <p> + * <p>{@code AbstractFuture} uses an {@link AbstractQueuedSynchronizer} to deal + * with concurrency issues and guarantee thread safety. + * <p> + * <p>The state changing methods all return a boolean indicating success or + * failure in changing the future's state. Valid states are running, + * completed, failed, or cancelled. + * <p> + * <p>This class uses an {@link ExecutionList} to guarantee that all registered + * listeners will be executed, either when the future finishes or, for listeners + * that are added after the future completes, immediately. + * {@code Runnable}-{@code Executor} pairs are stored in the execution list but + * are not necessarily executed in the order in which they were added. (If a + * listener is added after the Future is complete, it will be executed + * immediately, even if earlier listeners have not been executed. Additionally, + * executors need not guarantee FIFO execution, or different listeners may run + * in different executors.) + * + * @author Sven Mawson + * @since 1.0 + */ +public abstract class AbstractFuture<V> implements ListenableFuture<V> { + + /** + * Synchronization control for AbstractFutures. + */ + private final Sync<V> sync = new Sync<V>(); + + // The execution list to hold our executors. + private final ExecutionList executionList = new ExecutionList(); + + /** + * Constructor for use by subclasses. + */ + AbstractFuture() { + } + + /* + * Improve the documentation of when InterruptedException is thrown. Our + * behavior matches the JDK's, but the JDK's documentation is misleading. + */ + + private static CancellationException cancellationExceptionWithCause( + Throwable cause) { + CancellationException exception = new CancellationException("Task was cancelled."); + exception.initCause(cause); + return exception; + } + + /* + * Improve the documentation of when InterruptedException is thrown. Our + * behavior matches the JDK's, but the JDK's documentation is misleading. + */ + + /** + * {@inheritDoc} + * <p> + * <p>The default {@link AbstractFuture} implementation throws {@code + * InterruptedException} if the current thread is interrupted before or during + * the call, even if the value is already available. + * + * @throws InterruptedException if the current thread was interrupted before + * or during the call (optional but recommended). + * @throws CancellationException {@inheritDoc} + */ + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, + TimeoutException, ExecutionException { + return sync.get(unit.toNanos(timeout)); + } + + /** + * {@inheritDoc} + * <p> + * <p>The default {@link AbstractFuture} implementation throws {@code + * InterruptedException} if the current thread is interrupted before or during + * the call, even if the value is already available. + * + * @throws InterruptedException if the current thread was interrupted before + * or during the call (optional but recommended). + * @throws CancellationException {@inheritDoc} + */ + @Override + public V get() throws InterruptedException, ExecutionException { + return sync.get(); + } + + @Override + public boolean isDone() { + return sync.isDone(); + } + + @Override + public boolean isCancelled() { + return sync.isCancelled(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (!sync.cancel(mayInterruptIfRunning)) { + return false; + } + executionList.execute(); + if (mayInterruptIfRunning) { + interruptTask(); + } + return true; + } + + /** + * Subclasses can override this method to implement interruption of the + * future's computation. The method is invoked automatically by a successful + * call to {@link #cancel(boolean) cancel(true)}. + * <p> + * <p>The default implementation does nothing. + * + * @since 10.0 + */ + private void interruptTask() { + } + + /** + * Returns true if this future was cancelled with {@code + * mayInterruptIfRunning} set to {@code true}. + * + * @since 14.0 + */ + final boolean wasInterrupted() { + return sync.wasInterrupted(); + } + + /** + * {@inheritDoc} + * + * @since 10.0 + */ + @Override + public void addListener(Runnable listener, Executor exec) { + executionList.add(listener, exec); + } + + /** + * Subclasses should invoke this method to set the result of the computation + * to {@code value}. This will set the state of the future to + * {@link AbstractFuture.Sync#COMPLETED} and invoke the listeners if the + * state was successfully changed. + * + * @param value the value that was the result of the task. + * @return true if the state was successfully changed. + */ + boolean set(V value) { + boolean result = sync.set(value); + if (result) { + executionList.execute(); + } + return result; + } + + /** + * Subclasses should invoke this method to set the result of the computation + * to an error, {@code throwable}. This will set the state of the future to + * {@link AbstractFuture.Sync#COMPLETED} and invoke the listeners if the + * state was successfully changed. + * + * @param throwable the exception that the task failed with. + * @return true if the state was successfully changed. + */ + boolean setException(Throwable throwable) { + boolean result = sync.setException(checkNotNull(throwable)); + if (result) { + executionList.execute(); + } + return result; + } + + /** + * <p>Following the contract of {@link AbstractQueuedSynchronizer} we create a + * private subclass to hold the synchronizer. This synchronizer is used to + * implement the blocking and waiting calls as well as to handle state changes + * in a thread-safe manner. The current state of the future is held in the + * Sync state, and the lock is released whenever the state changes to + * {@link #COMPLETED}, {@link #CANCELLED}, or {@link #INTERRUPTED} + * <p> + * <p>To avoid races between threads doing release and acquire, we transition + * to the final state in two steps. One thread will successfully CAS from + * RUNNING to COMPLETING, that thread will then set the result of the + * computation, and only then transition to COMPLETED, CANCELLED, or + * INTERRUPTED. + * <p> + * <p>We don't use the integer argument passed between acquire methods so we + * pass around a -1 everywhere. + */ + static final class Sync<V> extends AbstractQueuedSynchronizer { + + /* Valid states. */ + static final int RUNNING = 0; + static final int COMPLETING = 1; + static final int COMPLETED = 2; + static final int CANCELLED = 4; + static final int INTERRUPTED = 8; + private static final long serialVersionUID = 0L; + private V value; + private Throwable exception; + + /* + * Acquisition succeeds if the future is done, otherwise it fails. + */ + @Override + protected int tryAcquireShared(int ignored) { + if (isDone()) { + return 1; + } + return -1; + } + + /* + * We always allow a release to go through, this means the state has been + * successfully changed and the result is available. + */ + @Override + protected boolean tryReleaseShared(int finalState) { + setState(finalState); + return true; + } + + /** + * Blocks until the task is complete or the timeout expires. Throws a + * {@link TimeoutException} if the timer expires, otherwise behaves like + * {@link #get()}. + */ + V get(long nanos) throws TimeoutException, CancellationException, + ExecutionException, InterruptedException { + + // Attempt to acquire the shared lock with a timeout. + if (!tryAcquireSharedNanos(-1, nanos)) { + throw new TimeoutException("Timeout waiting for task."); + } + + return getValue(); + } + + /** + * Blocks until {@link #complete(Object, Throwable, int)} has been + * successfully called. Throws a {@link CancellationException} if the task + * was cancelled, or a {@link ExecutionException} if the task completed with + * an error. + */ + V get() throws CancellationException, ExecutionException, + InterruptedException { + + // Acquire the shared lock allowing interruption. + acquireSharedInterruptibly(-1); + return getValue(); + } + + /** + * Implementation of the actual value retrieval. Will return the value + * on success, an exception on failure, a cancellation on cancellation, or + * an illegal state if the synchronizer is in an invalid state. + */ + private V getValue() throws CancellationException, ExecutionException { + int state = getState(); + switch (state) { + case COMPLETED: + if (exception != null) { + throw new ExecutionException(exception); + } else { + return value; + } + + case CANCELLED: + case INTERRUPTED: + throw cancellationExceptionWithCause( + exception); + + default: + throw new IllegalStateException( + "Error, synchronizer in invalid state: " + state); + } + } + + /** + * Checks if the state is {@link #COMPLETED}, {@link #CANCELLED}, or {@link + * INTERRUPTED}. + */ + boolean isDone() { + return (getState() & (COMPLETED | CANCELLED | INTERRUPTED)) != 0; + } + + /** + * Checks if the state is {@link #CANCELLED} or {@link #INTERRUPTED}. + */ + boolean isCancelled() { + return (getState() & (CANCELLED | INTERRUPTED)) != 0; + } + + /** + * Checks if the state is {@link #INTERRUPTED}. + */ + boolean wasInterrupted() { + return getState() == INTERRUPTED; + } + + /** + * Transition to the COMPLETED state and set the value. + */ + boolean set(V v) { + return complete(v, null, COMPLETED); + } + + /** + * Transition to the COMPLETED state and set the exception. + */ + boolean setException(Throwable t) { + return complete(null, t, COMPLETED); + } + + /** + * Transition to the CANCELLED or INTERRUPTED state. + */ + boolean cancel(boolean interrupt) { + return complete(null, null, interrupt ? INTERRUPTED : CANCELLED); + } + + /** + * Implementation of completing a task. Either {@code v} or {@code t} will + * be set but not both. The {@code finalState} is the state to change to + * from {@link #RUNNING}. If the state is not in the RUNNING state we + * return {@code false} after waiting for the state to be set to a valid + * final state ({@link #COMPLETED}, {@link #CANCELLED}, or {@link + * #INTERRUPTED}). + * + * @param v the value to set as the result of the computation. + * @param t the exception to set as the result of the computation. + * @param finalState the state to transition to. + */ + private boolean complete(V v, Throwable t, + int finalState) { + boolean doCompletion = compareAndSetState(RUNNING, COMPLETING); + if (doCompletion) { + // If this thread successfully transitioned to COMPLETING, set the value + // and exception and then release to the final state. + this.value = v; + // Don't actually construct a CancellationException until necessary. + this.exception = ((finalState & (CANCELLED | INTERRUPTED)) != 0) + ? new CancellationException("Future.cancel() was called.") : t; + releaseShared(finalState); + } else if (getState() == COMPLETING) { + // If some other thread is currently completing the future, block until + // they are done so we can guarantee completion. + acquireShared(-1); + } + return doCompletion; + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractIndexedListIterator.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractIndexedListIterator.java new file mode 100644 index 0000000..b9302af --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractIndexedListIterator.java
@@ -0,0 +1,94 @@ +/* + * Copyright (C) 2009 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.ListIterator; +import java.util.NoSuchElementException; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkPositionIndex; + +/** + * This class provides a skeletal implementation of the {@link ListIterator} + * interface across a fixed number of elements that may be retrieved by + * position. It does not support {@link #remove}, {@link #set}, or {@link #add}. + * + * @author Jared Levy + */ +abstract class AbstractIndexedListIterator<E> + extends UnmodifiableListIterator<E> { + private final int size; + private int position; + + /** + * Constructs an iterator across a sequence of the given size with the given + * initial position. That is, the first call to {@link #nextIndex()} will + * return {@code position}, and the first call to {@link #next()} will return + * the element at that index, if available. Calls to {@link #previous()} can + * retrieve the preceding {@code position} elements. + * + * @throws IndexOutOfBoundsException if {@code position} is negative or is + * greater than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + AbstractIndexedListIterator(int size, int position) { + checkPositionIndex(position, size); + this.size = size; + this.position = position; + } + + /** + * Returns the element with the specified index. This method is called by + * {@link #next()}. + */ + protected abstract E get(int index); + + @Override + public final boolean hasNext() { + return position < size; + } + + @Override + public final E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return get(position++); + } + + @Override + public final int nextIndex() { + return position; + } + + @Override + public final boolean hasPrevious() { + return position > 0; + } + + @Override + public final E previous() { + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + return get(--position); + } + + @Override + public final int previousIndex() { + return position - 1; + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractIterator.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractIterator.java new file mode 100644 index 0000000..c312385 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractIterator.java
@@ -0,0 +1,169 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.NoSuchElementException; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkState; + +/** + * This class provides a skeletal implementation of the {@code Iterator} + * interface, to make this interface easier to implement for certain types of + * data sources. + * <p> + * <p>{@code Iterator} requires its implementations to support querying the + * end-of-data status without changing the iterator's state, using the {@link + * #hasNext} method. But many data sources, such as {@link + * java.io.Reader#read()}, do not expose this information; the only way to + * discover whether there is any data left is by trying to retrieve it. These + * types of data sources are ordinarily difficult to write iterators for. But + * using this class, one must implement only the {@link #computeNext} method, + * and invoke the {@link #endOfData} method when appropriate. + * <p> + * <p>Another example is an iterator that skips over null elements in a backing + * iterator. This could be implemented as: <pre> {@code + * <p> + * public static Iterator<String> skipNulls(final Iterator<String> in) { + * return new AbstractIterator<String>() { + * protected String computeNext() { + * while (in.hasNext()) { + * String s = in.next(); + * if (s != null) { + * return s; + * } + * } + * return endOfData(); + * } + * }; + * }}</pre> + * <p> + * <p>This class supports iterators that include null elements. + * + * @author Kevin Bourrillion + * @since 2.0 (imported from Google Collections Library) + */ +// When making changes to this class, please also update the copy at +// org.glassfish.jersey.internal.guava.common.base.AbstractIterator +public abstract class AbstractIterator<T> extends UnmodifiableIterator<T> { + private State state = State.NOT_READY; + private T next; + + /** + * Constructor for use by subclasses. + */ + AbstractIterator() { + } + + /** + * Returns the next element. <b>Note:</b> the implementation must call {@link + * #endOfData()} when there are no elements left in the iteration. Failure to + * do so could result in an infinite loop. + * <p> + * <p>The initial invocation of {@link #hasNext()} or {@link #next()} calls + * this method, as does the first invocation of {@code hasNext} or {@code + * next} following each successful call to {@code next}. Once the + * implementation either invokes {@code endOfData} or throws an exception, + * {@code computeNext} is guaranteed to never be called again. + * <p> + * <p>If this method throws an exception, it will propagate outward to the + * {@code hasNext} or {@code next} invocation that invoked this method. Any + * further attempts to use the iterator will result in an {@link + * IllegalStateException}. + * <p> + * <p>The implementation of this method may not invoke the {@code hasNext}, + * {@code next}, or {@link #peek()} methods on this instance; if it does, an + * {@code IllegalStateException} will result. + * + * @return the next element if there was one. If {@code endOfData} was called + * during execution, the return value will be ignored. + * @throws RuntimeException if any unrecoverable error happens. This exception + * will propagate outward to the {@code hasNext()}, {@code next()}, or + * {@code peek()} invocation that invoked this method. Any further + * attempts to use the iterator will result in an + * {@link IllegalStateException}. + */ + protected abstract T computeNext(); + + /** + * Implementations of {@link #computeNext} <b>must</b> invoke this method when + * there are no elements left in the iteration. + * + * @return {@code null}; a convenience so your {@code computeNext} + * implementation can use the simple statement {@code return endOfData();} + */ + final T endOfData() { + state = State.DONE; + return null; + } + + @Override + public final boolean hasNext() { + checkState(state != State.FAILED); + switch (state) { + case DONE: + return false; + case READY: + return true; + default: + } + return tryToComputeNext(); + } + + private boolean tryToComputeNext() { + state = State.FAILED; // temporary pessimism + next = computeNext(); + if (state != State.DONE) { + state = State.READY; + return true; + } + return false; + } + + @Override + public final T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + state = State.NOT_READY; + T result = next; + next = null; + return result; + } + + private enum State { + /** + * We have computed the next element and haven't returned it yet. + */ + READY, + + /** + * We haven't yet computed or have already returned the element. + */ + NOT_READY, + + /** + * We have reached the end of the data and are finished. + */ + DONE, + + /** + * We've suffered an exception and are kaput. + */ + FAILED, + } + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractListMultimap.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractListMultimap.java new file mode 100644 index 0000000..2a03be7 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractListMultimap.java
@@ -0,0 +1,109 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Basic implementation of the {@link ListMultimap} interface. It's a wrapper + * around {@link AbstractMapBasedMultimap} that converts the returned collections into + * {@code Lists}. The {@link #createCollection} method must return a {@code + * List}. + * + * @author Jared Levy + * @since 2.0 (imported from Google Collections Library) + */ +abstract class AbstractListMultimap<K, V> + extends AbstractMapBasedMultimap<K, V> implements ListMultimap<K, V> { + private static final long serialVersionUID = 6588350623831699109L; + + /** + * Creates a new multimap that uses the provided map. + * + * @param map place to store the mapping from each key to its corresponding + * values + */ + AbstractListMultimap(Map<K, Collection<V>> map) { + super(map); + } + + @Override + abstract List<V> createCollection(); + + // Following Javadoc copied from ListMultimap. + + /** + * {@inheritDoc} + * <p> + * <p>Because the values for a given key may have duplicates and follow the + * insertion ordering, this method returns a {@link List}, instead of the + * {@link Collection} specified in the {@link Multimap} interface. + */ + @Override + public List<V> get(K key) { + return (List<V>) super.get(key); + } + + /** + * {@inheritDoc} + * <p> + * <p>Because the values for a given key may have duplicates and follow the + * insertion ordering, this method returns a {@link List}, instead of the + * {@link Collection} specified in the {@link Multimap} interface. + */ + @Override + public List<V> removeAll(Object key) { + return (List<V>) super.removeAll(key); + } + + /** + * Stores a key-value pair in the multimap. + * + * @param key key to store in the multimap + * @param value value to store in the multimap + * @return {@code true} always + */ + @Override + public boolean put(K key, V value) { + return super.put(key, value); + } + + /** + * {@inheritDoc} + * <p> + * <p>Though the method signature doesn't say so explicitly, the returned map + * has {@link List} values. + */ + @Override + public Map<K, Collection<V>> asMap() { + return super.asMap(); + } + + /** + * Compares the specified object to this multimap for equality. + * <p> + * <p>Two {@code ListMultimap} instances are equal if, for each key, they + * contain the same values in the same order. If the value orderings disagree, + * the multimaps will not be considered equal. + */ + @Override + public boolean equals(Object object) { + return super.equals(object); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMapBasedMultimap.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMapBasedMultimap.java new file mode 100644 index 0000000..c7a4c8e --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMapBasedMultimap.java
@@ -0,0 +1,1562 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.RandomAccess; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkArgument; +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; + +/** + * Basic implementation of the {@link Multimap} interface. This class represents + * a multimap as a map that associates each key with a collection of values. All + * methods of {@link Multimap} are supported, including those specified as + * optional in the interface. + * <p> + * <p>To implement a multimap, a subclass must define the method {@link + * #createCollection()}, which creates an empty collection of values for a key. + * <p> + * <p>The multimap constructor takes a map that has a single entry for each + * distinct key. When you insert a key-value pair with a key that isn't already + * in the multimap, {@code AbstractMapBasedMultimap} calls {@link #createCollection()} + * to create the collection of values for that key. The subclass should not call + * {@link #createCollection()} directly, and a new instance should be created + * every time the method is called. + * <p> + * <p>For example, the subclass could pass a {@link java.util.TreeMap} during + * construction, and {@link #createCollection()} could return a {@link + * java.util.TreeSet}, in which case the multimap's iterators would propagate + * through the keys and values in sorted order. + * <p> + * <p>Keys and values may be null, as long as the underlying collection classes + * support null elements. + * <p> + * <p>The collections created by {@link #createCollection()} may or may not + * allow duplicates. If the collection, such as a {@link Set}, does not support + * duplicates, an added key-value pair will replace an existing pair with the + * same key and value, if such a pair is present. With collections like {@link + * List} that allow duplicates, the collection will keep the existing key-value + * pairs while adding a new pair. + * <p> + * <p>This class is not threadsafe when any concurrent operations update the + * multimap, even if the underlying map and {@link #createCollection()} method + * return threadsafe classes. Concurrent read operations will work correctly. To + * allow concurrent update operations, wrap your multimap with a call to {@link + * Multimaps#synchronizedMultimap}. + * <p> + * <p>For serialization to work, the subclass must specify explicit + * {@code readObject} and {@code writeObject} methods. + * + * @author Jared Levy + * @author Louis Wasserman + */ +abstract class AbstractMapBasedMultimap<K, V> extends AbstractMultimap<K, V> + implements Serializable { + /* + * Here's an outline of the overall design. + * + * The map variable contains the collection of values associated with each + * key. When a key-value pair is added to a multimap that didn't previously + * contain any values for that key, a new collection generated by + * createCollection is added to the map. That same collection instance + * remains in the map as long as the multimap has any values for the key. If + * all values for the key are removed, the key and collection are removed + * from the map. + * + * The get method returns a WrappedCollection, which decorates the collection + * in the map (if the key is present) or an empty collection (if the key is + * not present). When the collection delegate in the WrappedCollection is + * empty, the multimap may contain subsequently added values for that key. To + * handle that situation, the WrappedCollection checks whether map contains + * an entry for the provided key, and if so replaces the delegate. + */ + + private static final long serialVersionUID = 2447537837011683357L; + private transient Map<K, Collection<V>> map; + private transient int totalSize; + + /** + * Creates a new multimap that uses the provided map. + * + * @param map place to store the mapping from each key to its corresponding + * values + * @throws IllegalArgumentException if {@code map} is not empty + */ + AbstractMapBasedMultimap(Map<K, Collection<V>> map) { + checkArgument(map.isEmpty()); + this.map = map; + } + + /** + * Used during deserialization only. + */ + final void setMap(Map<K, Collection<V>> map) { + this.map = map; + totalSize = 0; + for (Collection<V> values : map.values()) { + checkArgument(!values.isEmpty()); + totalSize += values.size(); + } + } + + /** + * Creates an unmodifiable, empty collection of values. + * <p> + * <p>This is used in {@link #removeAll} on an empty key. + */ + Collection<V> createUnmodifiableEmptyCollection() { + return unmodifiableCollectionSubclass(createCollection()); + } + + /** + * Creates the collection of values for a single key. + * <p> + * <p>Collections with weak, soft, or phantom references are not supported. + * Each call to {@code createCollection} should create a new instance. + * <p> + * <p>The returned collection class determines whether duplicate key-value + * pairs are allowed. + * + * @return an empty collection of values + */ + abstract Collection<V> createCollection(); + + /** + * Creates the collection of values for an explicitly provided key. By + * default, it simply calls {@link #createCollection()}, which is the correct + * behavior for most implementations. The {@link LinkedHashMultimap} class + * overrides it. + * + * @param key key to associate with values in the collection + * @return an empty collection of values + */ + Collection<V> createCollection(K key) { + return createCollection(); + } + + // Query Operations + + Map<K, Collection<V>> backingMap() { + return map; + } + + @Override + public int size() { + return totalSize; + } + + // Modification Operations + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean put(K key, V value) { + Collection<V> collection = map.get(key); + if (collection == null) { + collection = createCollection(key); + if (collection.add(value)) { + totalSize++; + map.put(key, collection); + return true; + } else { + throw new AssertionError("New Collection violated the Collection spec"); + } + } else if (collection.add(value)) { + totalSize++; + return true; + } else { + return false; + } + } + + // Bulk Operations + + /** + * {@inheritDoc} + * <p> + * <p>The returned collection is immutable. + */ + @Override + public Collection<V> removeAll(Object key) { + Collection<V> collection = map.remove(key); + + if (collection == null) { + return createUnmodifiableEmptyCollection(); + } + + Collection<V> output = createCollection(); + output.addAll(collection); + totalSize -= collection.size(); + collection.clear(); + + return unmodifiableCollectionSubclass(output); + } + + Collection<V> unmodifiableCollectionSubclass(Collection<V> collection) { + // We don't deal with NavigableSet here yet for GWT reasons -- instead, + // non-GWT TreeMultimap explicitly overrides this and uses NavigableSet. + if (collection instanceof SortedSet) { + return Collections.unmodifiableSortedSet((SortedSet<V>) collection); + } else if (collection instanceof Set) { + return Collections.unmodifiableSet((Set<V>) collection); + } else if (collection instanceof List) { + return Collections.unmodifiableList((List<V>) collection); + } else { + return Collections.unmodifiableCollection(collection); + } + } + + // Views + + @Override + public void clear() { + // Clear each collection, to make previously returned collections empty. + for (Collection<V> collection : map.values()) { + collection.clear(); + } + map.clear(); + totalSize = 0; + } + + /** + * {@inheritDoc} + * <p> + * <p>The returned collection is not serializable. + */ + @Override + public Collection<V> get(K key) { + Collection<V> collection = map.get(key); + if (collection == null) { + collection = createCollection(key); + } + return wrapCollection(key, collection); + } + + /** + * Generates a decorated collection that remains consistent with the values in + * the multimap for the provided key. Changes to the multimap may alter the + * returned collection, and vice versa. + */ + Collection<V> wrapCollection(K key, Collection<V> collection) { + // We don't deal with NavigableSet here yet for GWT reasons -- instead, + // non-GWT TreeMultimap explicitly overrides this and uses NavigableSet. + if (collection instanceof SortedSet) { + return new WrappedSortedSet(key, (SortedSet<V>) collection, null); + } else if (collection instanceof Set) { + return new WrappedSet(key, (Set<V>) collection); + } else if (collection instanceof List) { + return wrapList(key, (List<V>) collection, null); + } else { + return new WrappedCollection(key, collection, null); + } + } + + private List<V> wrapList( + K key, List<V> list, WrappedCollection ancestor) { + return (list instanceof RandomAccess) + ? new RandomAccessWrappedList(key, list, ancestor) + : new WrappedList(key, list, ancestor); + } + + private Iterator<V> iteratorOrListIterator(Collection<V> collection) { + return (collection instanceof List) + ? ((List<V>) collection).listIterator() + : collection.iterator(); + } + + @Override + Set<K> createKeySet() { + // TreeMultimap uses NavigableKeySet explicitly, but we don't handle that here for GWT + // compatibility reasons + return (map instanceof SortedMap) + ? new SortedKeySet((SortedMap<K, Collection<V>>) map) : new KeySet(map); + } + + /** + * Removes all values for the provided key. Unlike {@link #removeAll}, it + * returns the number of removed mappings. + */ + private int removeValuesForKey(Object key) { + Collection<V> collection = Maps.safeRemove(map, key); + + int count = 0; + if (collection != null) { + count = collection.size(); + collection.clear(); + totalSize -= count; + } + return count; + } + + /** + * {@inheritDoc} + * <p> + * <p>The iterator generated by the returned collection traverses the values + * for one key, followed by the values of a second key, and so on. + */ + @Override + public Collection<V> values() { + return super.values(); + } + + @Override + Iterator<V> valueIterator() { + return new Itr<V>() { + @Override + V output(K key, V value) { + return value; + } + }; + } + + /** + * {@inheritDoc} + * <p> + * <p>The iterator generated by the returned collection traverses the values + * for one key, followed by the values of a second key, and so on. + * <p> + * <p>Each entry is an immutable snapshot of a key-value mapping in the + * multimap, taken at the time the entry is returned by a method call to the + * collection or its iterator. + */ + @Override + public Collection<Entry<K, V>> entries() { + return super.entries(); + } + + /** + * Returns an iterator across all key-value map entries, used by {@code + * entries().iterator()} and {@code values().iterator()}. The default + * behavior, which traverses the values for one key, the values for a second + * key, and so on, suffices for most {@code AbstractMapBasedMultimap} implementations. + * + * @return an iterator across map entries + */ + @Override + Iterator<Entry<K, V>> entryIterator() { + return new Itr<Entry<K, V>>() { + @Override + Entry<K, V> output(K key, V value) { + return Maps.immutableEntry(key, value); + } + }; + } + + @Override + Map<K, Collection<V>> createAsMap() { + // TreeMultimap uses NavigableAsMap explicitly, but we don't handle that here for GWT + // compatibility reasons + return (map instanceof SortedMap) + ? new SortedAsMap((SortedMap<K, Collection<V>>) map) : new AsMap(map); + } + + /** + * Collection decorator that stays in sync with the multimap values for a key. + * There are two kinds of wrapped collections: full and subcollections. Both + * have a delegate pointing to the underlying collection class. + * <p> + * <p>Full collections, identified by a null ancestor field, contain all + * multimap values for a given key. Its delegate is a value in {@link + * AbstractMapBasedMultimap#map} whenever the delegate is non-empty. The {@code + * refreshIfEmpty}, {@code removeIfEmpty}, and {@code addToMap} methods ensure + * that the {@code WrappedCollection} and map remain consistent. + * <p> + * <p>A subcollection, such as a sublist, contains some of the values for a + * given key. Its ancestor field points to the full wrapped collection with + * all values for the key. The subcollection {@code refreshIfEmpty}, {@code + * removeIfEmpty}, and {@code addToMap} methods call the corresponding methods + * of the full wrapped collection. + */ + private class WrappedCollection extends AbstractCollection<V> { + final K key; + final WrappedCollection ancestor; + final Collection<V> ancestorDelegate; + Collection<V> delegate; + + WrappedCollection(K key, Collection<V> delegate, + WrappedCollection ancestor) { + this.key = key; + this.delegate = delegate; + this.ancestor = ancestor; + this.ancestorDelegate + = (ancestor == null) ? null : ancestor.getDelegate(); + } + + /** + * If the delegate collection is empty, but the multimap has values for the + * key, replace the delegate with the new collection for the key. + * <p> + * <p>For a subcollection, refresh its ancestor and validate that the + * ancestor delegate hasn't changed. + */ + void refreshIfEmpty() { + if (ancestor != null) { + ancestor.refreshIfEmpty(); + if (ancestor.getDelegate() != ancestorDelegate) { + throw new ConcurrentModificationException(); + } + } else if (delegate.isEmpty()) { + Collection<V> newDelegate = map.get(key); + if (newDelegate != null) { + delegate = newDelegate; + } + } + } + + /** + * If collection is empty, remove it from {@code AbstractMapBasedMultimap.this.map}. + * For subcollections, check whether the ancestor collection is empty. + */ + void removeIfEmpty() { + if (ancestor != null) { + ancestor.removeIfEmpty(); + } else if (delegate.isEmpty()) { + map.remove(key); + } + } + + K getKey() { + return key; + } + + /** + * Add the delegate to the map. Other {@code WrappedCollection} methods + * should call this method after adding elements to a previously empty + * collection. + * <p> + * <p>Subcollection add the ancestor's delegate instead. + */ + void addToMap() { + if (ancestor != null) { + ancestor.addToMap(); + } else { + map.put(key, delegate); + } + } + + @Override + public int size() { + refreshIfEmpty(); + return delegate.size(); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + refreshIfEmpty(); + return delegate.equals(object); + } + + @Override + public int hashCode() { + refreshIfEmpty(); + return delegate.hashCode(); + } + + @Override + public String toString() { + refreshIfEmpty(); + return delegate.toString(); + } + + Collection<V> getDelegate() { + return delegate; + } + + @Override + public Iterator<V> iterator() { + refreshIfEmpty(); + return new WrappedIterator(); + } + + @Override + public boolean add(V value) { + refreshIfEmpty(); + boolean wasEmpty = delegate.isEmpty(); + boolean changed = delegate.add(value); + if (changed) { + totalSize++; + if (wasEmpty) { + addToMap(); + } + } + return changed; + } + + WrappedCollection getAncestor() { + return ancestor; + } + + @Override + public boolean addAll(Collection<? extends V> collection) { + if (collection.isEmpty()) { + return false; + } + int oldSize = size(); // calls refreshIfEmpty + boolean changed = delegate.addAll(collection); + if (changed) { + int newSize = delegate.size(); + totalSize += (newSize - oldSize); + if (oldSize == 0) { + addToMap(); + } + } + return changed; + } + + // The following methods are provided for better performance. + + @Override + public boolean contains(Object o) { + refreshIfEmpty(); + return delegate.contains(o); + } + + @Override + public boolean containsAll(Collection<?> c) { + refreshIfEmpty(); + return delegate.containsAll(c); + } + + @Override + public void clear() { + int oldSize = size(); // calls refreshIfEmpty + if (oldSize == 0) { + return; + } + delegate.clear(); + totalSize -= oldSize; + removeIfEmpty(); // maybe shouldn't be removed if this is a sublist + } + + @Override + public boolean remove(Object o) { + refreshIfEmpty(); + boolean changed = delegate.remove(o); + if (changed) { + totalSize--; + removeIfEmpty(); + } + return changed; + } + + @Override + public boolean removeAll(Collection<?> c) { + if (c.isEmpty()) { + return false; + } + int oldSize = size(); // calls refreshIfEmpty + boolean changed = delegate.removeAll(c); + if (changed) { + int newSize = delegate.size(); + totalSize += (newSize - oldSize); + removeIfEmpty(); + } + return changed; + } + + @Override + public boolean retainAll(Collection<?> c) { + checkNotNull(c); + int oldSize = size(); // calls refreshIfEmpty + boolean changed = delegate.retainAll(c); + if (changed) { + int newSize = delegate.size(); + totalSize += (newSize - oldSize); + removeIfEmpty(); + } + return changed; + } + + /** + * Collection iterator for {@code WrappedCollection}. + */ + class WrappedIterator implements Iterator<V> { + final Iterator<V> delegateIterator; + final Collection<V> originalDelegate = delegate; + + WrappedIterator() { + delegateIterator = iteratorOrListIterator(delegate); + } + + WrappedIterator(Iterator<V> delegateIterator) { + this.delegateIterator = delegateIterator; + } + + /** + * If the delegate changed since the iterator was created, the iterator is + * no longer valid. + */ + void validateIterator() { + refreshIfEmpty(); + if (delegate != originalDelegate) { + throw new ConcurrentModificationException(); + } + } + + @Override + public boolean hasNext() { + validateIterator(); + return delegateIterator.hasNext(); + } + + @Override + public V next() { + validateIterator(); + return delegateIterator.next(); + } + + @Override + public void remove() { + delegateIterator.remove(); + totalSize--; + removeIfEmpty(); + } + + Iterator<V> getDelegateIterator() { + validateIterator(); + return delegateIterator; + } + } + } + + /** + * Set decorator that stays in sync with the multimap values for a key. + */ + private class WrappedSet extends WrappedCollection implements Set<V> { + WrappedSet(K key, Set<V> delegate) { + super(key, delegate, null); + } + + @Override + public boolean removeAll(Collection<?> c) { + if (c.isEmpty()) { + return false; + } + int oldSize = size(); // calls refreshIfEmpty + + // Guava issue 1013: AbstractSet and most JDK set implementations are + // susceptible to quadratic removeAll performance on lists; + // use a slightly smarter implementation here + boolean changed = Sets.removeAllImpl((Set<V>) delegate, c); + if (changed) { + int newSize = delegate.size(); + totalSize += (newSize - oldSize); + removeIfEmpty(); + } + return changed; + } + } + + /** + * SortedSet decorator that stays in sync with the multimap values for a key. + */ + private class WrappedSortedSet extends WrappedCollection + implements SortedSet<V> { + WrappedSortedSet(K key, SortedSet<V> delegate, + WrappedCollection ancestor) { + super(key, delegate, ancestor); + } + + SortedSet<V> getSortedSetDelegate() { + return (SortedSet<V>) getDelegate(); + } + + @Override + public Comparator<? super V> comparator() { + return getSortedSetDelegate().comparator(); + } + + @Override + public V first() { + refreshIfEmpty(); + return getSortedSetDelegate().first(); + } + + @Override + public V last() { + refreshIfEmpty(); + return getSortedSetDelegate().last(); + } + + @Override + public SortedSet<V> headSet(V toElement) { + refreshIfEmpty(); + return new WrappedSortedSet( + getKey(), getSortedSetDelegate().headSet(toElement), + (getAncestor() == null) ? this : getAncestor()); + } + + @Override + public SortedSet<V> subSet(V fromElement, V toElement) { + refreshIfEmpty(); + return new WrappedSortedSet( + getKey(), getSortedSetDelegate().subSet(fromElement, toElement), + (getAncestor() == null) ? this : getAncestor()); + } + + @Override + public SortedSet<V> tailSet(V fromElement) { + refreshIfEmpty(); + return new WrappedSortedSet( + getKey(), getSortedSetDelegate().tailSet(fromElement), + (getAncestor() == null) ? this : getAncestor()); + } + } + + class WrappedNavigableSet extends WrappedSortedSet implements NavigableSet<V> { + WrappedNavigableSet( + K key, NavigableSet<V> delegate, WrappedCollection ancestor) { + super(key, delegate, ancestor); + } + + @Override + NavigableSet<V> getSortedSetDelegate() { + return (NavigableSet<V>) super.getSortedSetDelegate(); + } + + @Override + public V lower(V v) { + return getSortedSetDelegate().lower(v); + } + + @Override + public V floor(V v) { + return getSortedSetDelegate().floor(v); + } + + @Override + public V ceiling(V v) { + return getSortedSetDelegate().ceiling(v); + } + + @Override + public V higher(V v) { + return getSortedSetDelegate().higher(v); + } + + @Override + public V pollFirst() { + return Iterators.pollNext(iterator()); + } + + @Override + public V pollLast() { + return Iterators.pollNext(descendingIterator()); + } + + private NavigableSet<V> wrap(NavigableSet<V> wrapped) { + return new WrappedNavigableSet(key, wrapped, + (getAncestor() == null) ? this : getAncestor()); + } + + @Override + public NavigableSet<V> descendingSet() { + return wrap(getSortedSetDelegate().descendingSet()); + } + + @Override + public Iterator<V> descendingIterator() { + return new AbstractMapBasedMultimap.WrappedCollection.WrappedIterator(getSortedSetDelegate() + .descendingIterator()); + } + + @Override + public NavigableSet<V> subSet( + V fromElement, boolean fromInclusive, V toElement, boolean toInclusive) { + return wrap( + getSortedSetDelegate().subSet(fromElement, fromInclusive, toElement, toInclusive)); + } + + @Override + public NavigableSet<V> headSet(V toElement, boolean inclusive) { + return wrap(getSortedSetDelegate().headSet(toElement, inclusive)); + } + + @Override + public NavigableSet<V> tailSet(V fromElement, boolean inclusive) { + return wrap(getSortedSetDelegate().tailSet(fromElement, inclusive)); + } + } + + /** + * List decorator that stays in sync with the multimap values for a key. + */ + private class WrappedList extends WrappedCollection implements List<V> { + WrappedList(K key, List<V> delegate, + WrappedCollection ancestor) { + super(key, delegate, ancestor); + } + + List<V> getListDelegate() { + return (List<V>) getDelegate(); + } + + @Override + public boolean addAll(int index, Collection<? extends V> c) { + if (c.isEmpty()) { + return false; + } + int oldSize = size(); // calls refreshIfEmpty + boolean changed = getListDelegate().addAll(index, c); + if (changed) { + int newSize = getDelegate().size(); + totalSize += (newSize - oldSize); + if (oldSize == 0) { + addToMap(); + } + } + return changed; + } + + @Override + public V get(int index) { + refreshIfEmpty(); + return getListDelegate().get(index); + } + + @Override + public V set(int index, V element) { + refreshIfEmpty(); + return getListDelegate().set(index, element); + } + + @Override + public void add(int index, V element) { + refreshIfEmpty(); + boolean wasEmpty = getDelegate().isEmpty(); + getListDelegate().add(index, element); + totalSize++; + if (wasEmpty) { + addToMap(); + } + } + + @Override + public V remove(int index) { + refreshIfEmpty(); + V value = getListDelegate().remove(index); + totalSize--; + removeIfEmpty(); + return value; + } + + @Override + public int indexOf(Object o) { + refreshIfEmpty(); + return getListDelegate().indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + refreshIfEmpty(); + return getListDelegate().lastIndexOf(o); + } + + @Override + public ListIterator<V> listIterator() { + refreshIfEmpty(); + return new WrappedListIterator(); + } + + @Override + public ListIterator<V> listIterator(int index) { + refreshIfEmpty(); + return new WrappedListIterator(index); + } + + @Override + public List<V> subList(int fromIndex, int toIndex) { + refreshIfEmpty(); + return wrapList( + getKey(), + getListDelegate().subList(fromIndex, toIndex), + (getAncestor() == null) ? this : getAncestor()); + } + + /** + * ListIterator decorator. + */ + private class WrappedListIterator extends WrappedIterator + implements ListIterator<V> { + WrappedListIterator() { + } + + public WrappedListIterator(int index) { + super(getListDelegate().listIterator(index)); + } + + private ListIterator<V> getDelegateListIterator() { + return (ListIterator<V>) getDelegateIterator(); + } + + @Override + public boolean hasPrevious() { + return getDelegateListIterator().hasPrevious(); + } + + @Override + public V previous() { + return getDelegateListIterator().previous(); + } + + @Override + public int nextIndex() { + return getDelegateListIterator().nextIndex(); + } + + @Override + public int previousIndex() { + return getDelegateListIterator().previousIndex(); + } + + @Override + public void set(V value) { + getDelegateListIterator().set(value); + } + + @Override + public void add(V value) { + boolean wasEmpty = isEmpty(); + getDelegateListIterator().add(value); + totalSize++; + if (wasEmpty) { + addToMap(); + } + } + } + } + + /** + * List decorator that stays in sync with the multimap values for a key and + * supports rapid random access. + */ + private class RandomAccessWrappedList extends WrappedList + implements RandomAccess { + RandomAccessWrappedList(K key, List<V> delegate, + WrappedCollection ancestor) { + super(key, delegate, ancestor); + } + } + + /* + * TODO(kevinb): should we copy this javadoc to each concrete class, so that + * classes like LinkedHashMultimap that need to say something different are + * still able to {@inheritDoc} all the way from Multimap? + */ + + private class KeySet extends Maps.KeySet<K, Collection<V>> { + KeySet(final Map<K, Collection<V>> subMap) { + super(subMap); + } + + @Override + public Iterator<K> iterator() { + final Iterator<Entry<K, Collection<V>>> entryIterator + = map().entrySet().iterator(); + return new Iterator<K>() { + Entry<K, Collection<V>> entry; + + @Override + public boolean hasNext() { + return entryIterator.hasNext(); + } + + @Override + public K next() { + entry = entryIterator.next(); + return entry.getKey(); + } + + @Override + public void remove() { + CollectPreconditions.checkRemove(entry != null); + Collection<V> collection = entry.getValue(); + entryIterator.remove(); + totalSize -= collection.size(); + collection.clear(); + } + }; + } + + // The following methods are included for better performance. + + @Override + public boolean remove(Object key) { + int count = 0; + Collection<V> collection = map().remove(key); + if (collection != null) { + count = collection.size(); + collection.clear(); + totalSize -= count; + } + return count > 0; + } + + @Override + public void clear() { + Iterators.clear(iterator()); + } + + @Override + public boolean containsAll(Collection<?> c) { + return map().keySet().containsAll(c); + } + + @Override + public boolean equals(Object object) { + return this == object || this.map().keySet().equals(object); + } + + @Override + public int hashCode() { + return map().keySet().hashCode(); + } + } + + private class SortedKeySet extends KeySet implements SortedSet<K> { + + SortedKeySet(SortedMap<K, Collection<V>> subMap) { + super(subMap); + } + + SortedMap<K, Collection<V>> sortedMap() { + return (SortedMap<K, Collection<V>>) super.map(); + } + + @Override + public Comparator<? super K> comparator() { + return sortedMap().comparator(); + } + + @Override + public K first() { + return sortedMap().firstKey(); + } + + @Override + public SortedSet<K> headSet(K toElement) { + return new SortedKeySet(sortedMap().headMap(toElement)); + } + + @Override + public K last() { + return sortedMap().lastKey(); + } + + @Override + public SortedSet<K> subSet(K fromElement, K toElement) { + return new SortedKeySet(sortedMap().subMap(fromElement, toElement)); + } + + @Override + public SortedSet<K> tailSet(K fromElement) { + return new SortedKeySet(sortedMap().tailMap(fromElement)); + } + } + + class NavigableKeySet extends SortedKeySet implements NavigableSet<K> { + NavigableKeySet(NavigableMap<K, Collection<V>> subMap) { + super(subMap); + } + + @Override + NavigableMap<K, Collection<V>> sortedMap() { + return (NavigableMap<K, Collection<V>>) super.sortedMap(); + } + + @Override + public K lower(K k) { + return sortedMap().lowerKey(k); + } + + @Override + public K floor(K k) { + return sortedMap().floorKey(k); + } + + @Override + public K ceiling(K k) { + return sortedMap().ceilingKey(k); + } + + @Override + public K higher(K k) { + return sortedMap().higherKey(k); + } + + @Override + public K pollFirst() { + return Iterators.pollNext(iterator()); + } + + @Override + public K pollLast() { + return Iterators.pollNext(descendingIterator()); + } + + @Override + public NavigableSet<K> descendingSet() { + return new NavigableKeySet(sortedMap().descendingMap()); + } + + @Override + public Iterator<K> descendingIterator() { + return descendingSet().iterator(); + } + + @Override + public NavigableSet<K> headSet(K toElement) { + return headSet(toElement, false); + } + + @Override + public NavigableSet<K> headSet(K toElement, boolean inclusive) { + return new NavigableKeySet(sortedMap().headMap(toElement, inclusive)); + } + + @Override + public NavigableSet<K> subSet(K fromElement, K toElement) { + return subSet(fromElement, true, toElement, false); + } + + @Override + public NavigableSet<K> subSet( + K fromElement, boolean fromInclusive, K toElement, boolean toInclusive) { + return new NavigableKeySet( + sortedMap().subMap(fromElement, fromInclusive, toElement, toInclusive)); + } + + @Override + public NavigableSet<K> tailSet(K fromElement) { + return tailSet(fromElement, true); + } + + @Override + public NavigableSet<K> tailSet(K fromElement, boolean inclusive) { + return new NavigableKeySet(sortedMap().tailMap(fromElement, inclusive)); + } + } + + private abstract class Itr<T> implements Iterator<T> { + final Iterator<Entry<K, Collection<V>>> keyIterator; + K key; + Collection<V> collection; + Iterator<V> valueIterator; + + Itr() { + keyIterator = map.entrySet().iterator(); + key = null; + collection = null; + valueIterator = Iterators.emptyModifiableIterator(); + } + + abstract T output(K key, V value); + + @Override + public boolean hasNext() { + return keyIterator.hasNext() || valueIterator.hasNext(); + } + + @Override + public T next() { + if (!valueIterator.hasNext()) { + Entry<K, Collection<V>> mapEntry = keyIterator.next(); + key = mapEntry.getKey(); + collection = mapEntry.getValue(); + valueIterator = collection.iterator(); + } + return output(key, valueIterator.next()); + } + + @Override + public void remove() { + valueIterator.remove(); + if (collection.isEmpty()) { + keyIterator.remove(); + } + totalSize--; + } + } + + private class AsMap extends Maps.ImprovedAbstractMap<K, Collection<V>> { + /** + * Usually the same as map, but smaller for the headMap(), tailMap(), or + * subMap() of a SortedAsMap. + */ + final transient Map<K, Collection<V>> submap; + + AsMap(Map<K, Collection<V>> submap) { + this.submap = submap; + } + + @Override + protected Set<Entry<K, Collection<V>>> createEntrySet() { + return new AsMapEntries(); + } + + // The following methods are included for performance. + + @Override + public boolean containsKey(Object key) { + return Maps.safeContainsKey(submap, key); + } + + @Override + public Collection<V> get(Object key) { + Collection<V> collection = Maps.safeGet(submap, key); + if (collection == null) { + return null; + } + @SuppressWarnings("unchecked") + K k = (K) key; + return wrapCollection(k, collection); + } + + @Override + public Set<K> keySet() { + return AbstractMapBasedMultimap.this.keySet(); + } + + @Override + public int size() { + return submap.size(); + } + + @Override + public Collection<V> remove(Object key) { + Collection<V> collection = submap.remove(key); + if (collection == null) { + return null; + } + + Collection<V> output = createCollection(); + output.addAll(collection); + totalSize -= collection.size(); + collection.clear(); + return output; + } + + @Override + public boolean equals(Object object) { + return this == object || submap.equals(object); + } + + @Override + public int hashCode() { + return submap.hashCode(); + } + + @Override + public String toString() { + return submap.toString(); + } + + @Override + public void clear() { + if (submap == map) { + AbstractMapBasedMultimap.this.clear(); + } else { + Iterators.clear(new AsMapIterator()); + } + } + + Entry<K, Collection<V>> wrapEntry(Entry<K, Collection<V>> entry) { + K key = entry.getKey(); + return Maps.immutableEntry(key, wrapCollection(key, entry.getValue())); + } + + class AsMapEntries extends Maps.EntrySet<K, Collection<V>> { + @Override + Map<K, Collection<V>> map() { + return AbstractMapBasedMultimap.AsMap.this; + } + + @Override + public Iterator<Entry<K, Collection<V>>> iterator() { + return new AsMapIterator(); + } + + // The following methods are included for performance. + + @Override + public boolean contains(Object o) { + return Collections2.safeContains(submap.entrySet(), o); + } + + @Override + public boolean remove(Object o) { + if (!contains(o)) { + return false; + } + Entry<?, ?> entry = (Entry<?, ?>) o; + removeValuesForKey(entry.getKey()); + return true; + } + } + + /** + * Iterator across all keys and value collections. + */ + class AsMapIterator implements Iterator<Entry<K, Collection<V>>> { + final Iterator<Entry<K, Collection<V>>> delegateIterator + = submap.entrySet().iterator(); + Collection<V> collection; + + @Override + public boolean hasNext() { + return delegateIterator.hasNext(); + } + + @Override + public Entry<K, Collection<V>> next() { + Entry<K, Collection<V>> entry = delegateIterator.next(); + collection = entry.getValue(); + return wrapEntry(entry); + } + + @Override + public void remove() { + delegateIterator.remove(); + totalSize -= collection.size(); + collection.clear(); + } + } + } + + private class SortedAsMap extends AsMap + implements SortedMap<K, Collection<V>> { + SortedSet<K> sortedKeySet; + + SortedAsMap(SortedMap<K, Collection<V>> submap) { + super(submap); + } + + SortedMap<K, Collection<V>> sortedMap() { + return (SortedMap<K, Collection<V>>) submap; + } + + @Override + public Comparator<? super K> comparator() { + return sortedMap().comparator(); + } + + @Override + public K firstKey() { + return sortedMap().firstKey(); + } + + @Override + public K lastKey() { + return sortedMap().lastKey(); + } + + @Override + public SortedMap<K, Collection<V>> headMap(K toKey) { + return new SortedAsMap(sortedMap().headMap(toKey)); + } + + @Override + public SortedMap<K, Collection<V>> subMap(K fromKey, K toKey) { + return new SortedAsMap(sortedMap().subMap(fromKey, toKey)); + } + + @Override + public SortedMap<K, Collection<V>> tailMap(K fromKey) { + return new SortedAsMap(sortedMap().tailMap(fromKey)); + } + + // returns a SortedSet, even though returning a Set would be sufficient to + // satisfy the SortedMap.keySet() interface + @Override + public SortedSet<K> keySet() { + SortedSet<K> result = sortedKeySet; + return (result == null) ? sortedKeySet = createKeySet() : result; + } + + @Override + SortedSet<K> createKeySet() { + return new SortedKeySet(sortedMap()); + } + } + + class NavigableAsMap extends SortedAsMap implements NavigableMap<K, Collection<V>> { + + NavigableAsMap(NavigableMap<K, Collection<V>> submap) { + super(submap); + } + + @Override + NavigableMap<K, Collection<V>> sortedMap() { + return (NavigableMap<K, Collection<V>>) super.sortedMap(); + } + + @Override + public Entry<K, Collection<V>> lowerEntry(K key) { + Entry<K, Collection<V>> entry = sortedMap().lowerEntry(key); + return (entry == null) ? null : wrapEntry(entry); + } + + @Override + public K lowerKey(K key) { + return sortedMap().lowerKey(key); + } + + @Override + public Entry<K, Collection<V>> floorEntry(K key) { + Entry<K, Collection<V>> entry = sortedMap().floorEntry(key); + return (entry == null) ? null : wrapEntry(entry); + } + + @Override + public K floorKey(K key) { + return sortedMap().floorKey(key); + } + + @Override + public Entry<K, Collection<V>> ceilingEntry(K key) { + Entry<K, Collection<V>> entry = sortedMap().ceilingEntry(key); + return (entry == null) ? null : wrapEntry(entry); + } + + @Override + public K ceilingKey(K key) { + return sortedMap().ceilingKey(key); + } + + @Override + public Entry<K, Collection<V>> higherEntry(K key) { + Entry<K, Collection<V>> entry = sortedMap().higherEntry(key); + return (entry == null) ? null : wrapEntry(entry); + } + + @Override + public K higherKey(K key) { + return sortedMap().higherKey(key); + } + + @Override + public Entry<K, Collection<V>> firstEntry() { + Entry<K, Collection<V>> entry = sortedMap().firstEntry(); + return (entry == null) ? null : wrapEntry(entry); + } + + @Override + public Entry<K, Collection<V>> lastEntry() { + Entry<K, Collection<V>> entry = sortedMap().lastEntry(); + return (entry == null) ? null : wrapEntry(entry); + } + + @Override + public Entry<K, Collection<V>> pollFirstEntry() { + return pollAsMapEntry(entrySet().iterator()); + } + + @Override + public Entry<K, Collection<V>> pollLastEntry() { + return pollAsMapEntry(descendingMap().entrySet().iterator()); + } + + Entry<K, Collection<V>> pollAsMapEntry(Iterator<Entry<K, Collection<V>>> entryIterator) { + if (!entryIterator.hasNext()) { + return null; + } + Entry<K, Collection<V>> entry = entryIterator.next(); + Collection<V> output = createCollection(); + output.addAll(entry.getValue()); + entryIterator.remove(); + return Maps.immutableEntry(entry.getKey(), unmodifiableCollectionSubclass(output)); + } + + @Override + public NavigableMap<K, Collection<V>> descendingMap() { + return new NavigableAsMap(sortedMap().descendingMap()); + } + + @Override + public NavigableSet<K> keySet() { + return (NavigableSet<K>) super.keySet(); + } + + @Override + NavigableSet<K> createKeySet() { + return new NavigableKeySet(sortedMap()); + } + + @Override + public NavigableSet<K> navigableKeySet() { + return keySet(); + } + + @Override + public NavigableSet<K> descendingKeySet() { + return descendingMap().navigableKeySet(); + } + + @Override + public NavigableMap<K, Collection<V>> subMap(K fromKey, K toKey) { + return subMap(fromKey, true, toKey, false); + } + + @Override + public NavigableMap<K, Collection<V>> subMap( + K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) { + return new NavigableAsMap(sortedMap().subMap(fromKey, fromInclusive, toKey, toInclusive)); + } + + @Override + public NavigableMap<K, Collection<V>> headMap(K toKey) { + return headMap(toKey, false); + } + + @Override + public NavigableMap<K, Collection<V>> headMap(K toKey, boolean inclusive) { + return new NavigableAsMap(sortedMap().headMap(toKey, inclusive)); + } + + @Override + public NavigableMap<K, Collection<V>> tailMap(K fromKey) { + return tailMap(fromKey, true); + } + + @Override + public NavigableMap<K, Collection<V>> tailMap(K fromKey, boolean inclusive) { + return new NavigableAsMap(sortedMap().tailMap(fromKey, inclusive)); + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMapEntry.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMapEntry.java new file mode 100644 index 0000000..581a4d2 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMapEntry.java
@@ -0,0 +1,65 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.Map.Entry; +import java.util.Objects; + +/** + * Implementation of the {@code equals}, {@code hashCode}, and {@code toString} + * methods of {@code Entry}. + * + * @author Jared Levy + */ +abstract class AbstractMapEntry<K, V> implements Entry<K, V> { + + @Override + public abstract K getKey(); + + @Override + public abstract V getValue(); + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof Entry) { + Entry<?, ?> that = (Entry<?, ?>) object; + return Objects.equals(this.getKey(), that.getKey()) + && Objects.equals(this.getValue(), that.getValue()); + } + return false; + } + + @Override + public int hashCode() { + K k = getKey(); + V v = getValue(); + return ((k == null) ? 0 : k.hashCode()) ^ ((v == null) ? 0 : v.hashCode()); + } + + /** + * Returns a string representation of the form {@code {key}={value}}. + */ + @Override + public String toString() { + return getKey() + "=" + getValue(); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMultimap.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMultimap.java new file mode 100644 index 0000000..17244fd --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractMultimap.java
@@ -0,0 +1,205 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; + +/** + * A skeleton {@code Multimap} implementation, not necessarily in terms of a {@code Map}. + * + * @author Louis Wasserman + */ +abstract class AbstractMultimap<K, V> implements Multimap<K, V> { + private transient Collection<Entry<K, V>> entries; + private transient Set<K> keySet; + private transient Collection<V> values; + private transient Map<K, Collection<V>> asMap; + + @Override + public boolean containsValue(Object value) { + for (Collection<V> collection : asMap().values()) { + if (collection.contains(value)) { + return true; + } + } + + return false; + } + + @Override + public boolean containsEntry(Object key, Object value) { + Collection<V> collection = asMap().get(key); + return collection != null && collection.contains(value); + } + + @Override + public boolean remove(Object key, Object value) { + Collection<V> collection = asMap().get(key); + return collection != null && collection.remove(value); + } + + @Override + public boolean put(K key, V value) { + return get(key).add(value); + } + + @Override + public boolean putAll(K key, Iterable<? extends V> values) { + checkNotNull(values); + // make sure we only call values.iterator() once + // and we only call get(key) if values is nonempty + if (values instanceof Collection) { + Collection<? extends V> valueCollection = (Collection<? extends V>) values; + return !valueCollection.isEmpty() && get(key).addAll(valueCollection); + } else { + Iterator<? extends V> valueItr = values.iterator(); + return valueItr.hasNext() && Iterators.addAll(get(key), valueItr); + } + } + + @Override + public Collection<Entry<K, V>> entries() { + Collection<Entry<K, V>> result = entries; + return (result == null) ? entries = createEntries() : result; + } + + private Collection<Entry<K, V>> createEntries() { + if (this instanceof SetMultimap) { + return new EntrySet(); + } else { + return new Entries(); + } + } + + abstract Iterator<Entry<K, V>> entryIterator(); + + @Override + public Set<K> keySet() { + Set<K> result = keySet; + return (result == null) ? keySet = createKeySet() : result; + } + + Set<K> createKeySet() { + return new Maps.KeySet<K, Collection<V>>(asMap()); + } + + @Override + public Collection<V> values() { + Collection<V> result = values; + return (result == null) ? values = createValues() : result; + } + + private Collection<V> createValues() { + return new Values(); + } + + Iterator<V> valueIterator() { + return Maps.valueIterator(entries().iterator()); + } + + @Override + public Map<K, Collection<V>> asMap() { + Map<K, Collection<V>> result = asMap; + return (result == null) ? asMap = createAsMap() : result; + } + + abstract Map<K, Collection<V>> createAsMap(); + + @Override + public boolean equals(Object object) { + return Multimaps.equalsImpl(this, object); + } + + /** + * Returns the hash code for this multimap. + * <p> + * <p>The hash code of a multimap is defined as the hash code of the map view, + * as returned by {@link Multimap#asMap}. + * + * @see Map#hashCode + */ + @Override + public int hashCode() { + return asMap().hashCode(); + } + + /** + * Returns a string representation of the multimap, generated by calling + * {@code toString} on the map returned by {@link Multimap#asMap}. + * + * @return a string representation of the multimap + */ + @Override + public String toString() { + return asMap().toString(); + } + + // Comparison and hashing + + private class Entries extends Multimaps.Entries<K, V> { + @Override + Multimap<K, V> multimap() { + return AbstractMultimap.this; + } + + @Override + public Iterator<Entry<K, V>> iterator() { + return entryIterator(); + } + } + + private class EntrySet extends Entries implements Set<Entry<K, V>> { + @Override + public int hashCode() { + return Sets.hashCodeImpl(this); + } + + @Override + public boolean equals(Object obj) { + return Sets.equalsImpl(this, obj); + } + } + + private class Values extends AbstractCollection<V> { + @Override + public Iterator<V> iterator() { + return valueIterator(); + } + + @Override + public int size() { + return AbstractMultimap.this.size(); + } + + @Override + public boolean contains(Object o) { + return AbstractMultimap.this.containsValue(o); + } + + @Override + public void clear() { + AbstractMultimap.this.clear(); + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSequentialIterator.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSequentialIterator.java new file mode 100644 index 0000000..dde2b14 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSequentialIterator.java
@@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.NoSuchElementException; + +/** + * This class provides a skeletal implementation of the {@code Iterator} + * interface for sequences whose next element can always be derived from the + * previous element. Null elements are not supported, nor is the + * {@link #remove()} method. + * <p> + * <p>Example: <pre> {@code + * <p> + * Iterator<Integer> powersOfTwo = + * new AbstractSequentialIterator<Integer>(1) { + * protected Integer computeNext(Integer previous) { + * return (previous == 1 << 30) ? null : previous * 2; + * } + * };}</pre> + * + * @author Chris Povirk + * @since 12.0 (in Guava as {@code AbstractLinkedIterator} since 8.0) + */ +abstract class AbstractSequentialIterator<T> + extends UnmodifiableIterator<T> { + private T nextOrNull; + + /** + * Creates a new iterator with the given first element, or, if {@code + * firstOrNull} is null, creates a new empty iterator. + */ + AbstractSequentialIterator(T firstOrNull) { + this.nextOrNull = firstOrNull; + } + + /** + * Returns the element that follows {@code previous}, or returns {@code null} + * if no elements remain. This method is invoked during each call to + * {@link #next()} in order to compute the result of a <i>future</i> call to + * {@code next()}. + */ + protected abstract T computeNext(T previous); + + @Override + public final boolean hasNext() { + return nextOrNull != null; + } + + @Override + public final T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + try { + return nextOrNull; + } finally { + nextOrNull = computeNext(nextOrNull); + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSetMultimap.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSetMultimap.java new file mode 100644 index 0000000..6247624 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSetMultimap.java
@@ -0,0 +1,126 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Basic implementation of the {@link SetMultimap} interface. It's a wrapper + * around {@link AbstractMapBasedMultimap} that converts the returned collections into + * {@code Sets}. The {@link #createCollection} method must return a {@code Set}. + * + * @author Jared Levy + */ +abstract class AbstractSetMultimap<K, V> + extends AbstractMapBasedMultimap<K, V> implements SetMultimap<K, V> { + private static final long serialVersionUID = 7431625294878419160L; + + /** + * Creates a new multimap that uses the provided map. + * + * @param map place to store the mapping from each key to its corresponding + * values + */ + AbstractSetMultimap(Map<K, Collection<V>> map) { + super(map); + } + + @Override + abstract Set<V> createCollection(); + + // Following Javadoc copied from SetMultimap. + + @Override + Set<V> createUnmodifiableEmptyCollection() { + return Collections.emptySet(); + } + + /** + * {@inheritDoc} + * <p> + * <p>Because a {@code SetMultimap} has unique values for a given key, this + * method returns a {@link Set}, instead of the {@link Collection} specified + * in the {@link Multimap} interface. + */ + @Override + public Set<V> get(K key) { + return (Set<V>) super.get(key); + } + + /** + * {@inheritDoc} + * <p> + * <p>Because a {@code SetMultimap} has unique values for a given key, this + * method returns a {@link Set}, instead of the {@link Collection} specified + * in the {@link Multimap} interface. + */ + @Override + public Set<Map.Entry<K, V>> entries() { + return (Set<Map.Entry<K, V>>) super.entries(); + } + + /** + * {@inheritDoc} + * <p> + * <p>Because a {@code SetMultimap} has unique values for a given key, this + * method returns a {@link Set}, instead of the {@link Collection} specified + * in the {@link Multimap} interface. + */ + @Override + public Set<V> removeAll(Object key) { + return (Set<V>) super.removeAll(key); + } + + /** + * {@inheritDoc} + * <p> + * <p>Though the method signature doesn't say so explicitly, the returned map + * has {@link Set} values. + */ + @Override + public Map<K, Collection<V>> asMap() { + return super.asMap(); + } + + /** + * Stores a key-value pair in the multimap. + * + * @param key key to store in the multimap + * @param value value to store in the multimap + * @return {@code true} if the method increased the size of the multimap, or + * {@code false} if the multimap already contained the key-value pair + */ + @Override + public boolean put(K key, V value) { + return super.put(key, value); + } + + /** + * Compares the specified object to this multimap for equality. + * <p> + * <p>Two {@code SetMultimap} instances are equal if, for each key, they + * contain the same values. Equality does not depend on the ordering of keys + * or values. + */ + @Override + public boolean equals(Object object) { + return super.equals(object); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSortedKeySortedSetMultimap.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSortedKeySortedSetMultimap.java new file mode 100644 index 0000000..813ed07 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSortedKeySortedSetMultimap.java
@@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.Collection; +import java.util.SortedMap; +import java.util.SortedSet; + +/** + * Basic implementation of a {@link SortedSetMultimap} with a sorted key set. + * <p> + * This superclass allows {@code TreeMultimap} to override methods to return + * navigable set and map types in non-GWT only, while GWT code will inherit the + * SortedMap/SortedSet overrides. + * + * @author Louis Wasserman + */ +abstract class AbstractSortedKeySortedSetMultimap<K, V> extends AbstractSortedSetMultimap<K, V> { + + AbstractSortedKeySortedSetMultimap(SortedMap<K, Collection<V>> map) { + super(map); + } + + @Override + public SortedMap<K, Collection<V>> asMap() { + return (SortedMap<K, Collection<V>>) super.asMap(); + } + + @Override + SortedMap<K, Collection<V>> backingMap() { + return (SortedMap<K, Collection<V>>) super.backingMap(); + } + + @Override + public SortedSet<K> keySet() { + return (SortedSet<K>) super.keySet(); + } + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSortedSetMultimap.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSortedSetMultimap.java new file mode 100644 index 0000000..fa547e5 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractSortedSetMultimap.java
@@ -0,0 +1,113 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.SortedSet; + +/** + * Basic implementation of the {@link SortedSetMultimap} interface. It's a + * wrapper around {@link AbstractMapBasedMultimap} that converts the returned + * collections into sorted sets. The {@link #createCollection} method + * must return a {@code SortedSet}. + * + * @author Jared Levy + */ +abstract class AbstractSortedSetMultimap<K, V> + extends AbstractSetMultimap<K, V> implements SortedSetMultimap<K, V> { + private static final long serialVersionUID = 430848587173315748L; + + /** + * Creates a new multimap that uses the provided map. + * + * @param map place to store the mapping from each key to its corresponding + * values + */ + AbstractSortedSetMultimap(Map<K, Collection<V>> map) { + super(map); + } + + @Override + abstract SortedSet<V> createCollection(); + + @Override + SortedSet<V> createUnmodifiableEmptyCollection() { + return Collections.unmodifiableSortedSet(createCollection()); + } + + /** + * Returns a collection view of all values associated with a key. If no + * mappings in the multimap have the provided key, an empty collection is + * returned. + * <p> + * <p>Changes to the returned collection will update the underlying multimap, + * and vice versa. + * <p> + * <p>Because a {@code SortedSetMultimap} has unique sorted values for a given + * key, this method returns a {@link SortedSet}, instead of the + * {@link Collection} specified in the {@link Multimap} interface. + */ + @Override + public SortedSet<V> get(K key) { + return (SortedSet<V>) super.get(key); + } + + /** + * Removes all values associated with a given key. The returned collection is + * immutable. + * <p> + * <p>Because a {@code SortedSetMultimap} has unique sorted values for a given + * key, this method returns a {@link SortedSet}, instead of the + * {@link Collection} specified in the {@link Multimap} interface. + */ + @Override + public SortedSet<V> removeAll(Object key) { + return (SortedSet<V>) super.removeAll(key); + } + + /** + * Returns a map view that associates each key with the corresponding values + * in the multimap. Changes to the returned map, such as element removal, will + * update the underlying multimap. The map does not support {@code setValue} + * on its entries, {@code put}, or {@code putAll}. + * <p> + * <p>When passed a key that is present in the map, {@code + * asMap().get(Object)} has the same behavior as {@link #get}, returning a + * live collection. When passed a key that is not present, however, {@code + * asMap().get(Object)} returns {@code null} instead of an empty collection. + * <p> + * <p>Though the method signature doesn't say so explicitly, the returned map + * has {@link SortedSet} values. + */ + @Override + public Map<K, Collection<V>> asMap() { + return super.asMap(); + } + + /** + * {@inheritDoc} + * <p> + * Consequently, the values do not follow their natural ordering or the + * ordering of the value comparator. + */ + @Override + public Collection<V> values() { + return super.values(); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractTable.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractTable.java new file mode 100644 index 0000000..1a3e5ac --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AbstractTable.java
@@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Skeletal, implementation-agnostic implementation of the {@link Table} interface. + * + * @author Louis Wasserman + */ +abstract class AbstractTable<R, C, V> implements Table<R, C, V> { + + private transient Set<Cell<R, C, V>> cellSet; + + @Override + public boolean containsRow(Object rowKey) { + return Maps.safeContainsKey(rowMap(), rowKey); + } + + @Override + public boolean containsColumn(Object columnKey) { + return Maps.safeContainsKey(columnMap(), columnKey); + } + + @Override + public Set<R> rowKeySet() { + return rowMap().keySet(); + } + + @Override + public Set<C> columnKeySet() { + return columnMap().keySet(); + } + + @Override + public boolean containsValue(Object value) { + for (Map<C, V> row : rowMap().values()) { + if (row.containsValue(value)) { + return true; + } + } + return false; + } + + @Override + public boolean contains(Object rowKey, Object columnKey) { + Map<C, V> row = Maps.safeGet(rowMap(), rowKey); + return row != null && Maps.safeContainsKey(row, columnKey); + } + + @Override + public V get(Object rowKey, Object columnKey) { + Map<C, V> row = Maps.safeGet(rowMap(), rowKey); + return (row == null) ? null : Maps.safeGet(row, columnKey); + } + + @Override + public void clear() { + Iterators.clear(cellSet().iterator()); + } + + @Override + public V remove(Object rowKey, Object columnKey) { + Map<C, V> row = Maps.safeGet(rowMap(), rowKey); + return (row == null) ? null : Maps.safeRemove(row, columnKey); + } + + @Override + public V put(R rowKey, C columnKey, V value) { + return row(rowKey).put(columnKey, value); + } + + @Override + public void putAll(Table<? extends R, ? extends C, ? extends V> table) { + for (Cell<? extends R, ? extends C, ? extends V> cell : table.cellSet()) { + put(cell.getRowKey(), cell.getColumnKey(), cell.getValue()); + } + } + + @Override + public Set<Cell<R, C, V>> cellSet() { + Set<Cell<R, C, V>> result = cellSet; + return (result == null) ? cellSet = createCellSet() : result; + } + + private Set<Cell<R, C, V>> createCellSet() { + return new CellSet(); + } + + abstract Iterator<Cell<R, C, V>> cellIterator(); + + @Override + public boolean equals(Object obj) { + return Tables.equalsImpl(this, obj); + } + + @Override + public int hashCode() { + return cellSet().hashCode(); + } + + /** + * Returns the string representation {@code rowMap().toString()}. + */ + @Override + public String toString() { + return rowMap().toString(); + } + + private class CellSet extends AbstractSet<Cell<R, C, V>> { + @Override + public boolean contains(Object o) { + if (o instanceof Cell) { + Cell<?, ?, ?> cell = (Cell<?, ?, ?>) o; + Map<C, V> row = Maps.safeGet(rowMap(), cell.getRowKey()); + return row != null && Collections2.safeContains( + row.entrySet(), Maps.immutableEntry(cell.getColumnKey(), cell.getValue())); + } + return false; + } + + @Override + public boolean remove(Object o) { + if (o instanceof Cell) { + Cell<?, ?, ?> cell = (Cell<?, ?, ?>) o; + Map<C, V> row = Maps.safeGet(rowMap(), cell.getRowKey()); + return row != null && Collections2.safeRemove( + row.entrySet(), Maps.immutableEntry(cell.getColumnKey(), cell.getValue())); + } + return false; + } + + @Override + public void clear() { + AbstractTable.this.clear(); + } + + @Override + public Iterator<Cell<R, C, V>> iterator() { + return cellIterator(); + } + + @Override + public int size() { + return AbstractTable.this.size(); + } + } + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/AsyncFunction.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AsyncFunction.java new file mode 100644 index 0000000..206ceac --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/AsyncFunction.java
@@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.concurrent.Future; + +/** + * Transforms a value, possibly asynchronously. For an example usage and more + * information, see {@link Futures#transform(ListenableFuture, AsyncFunction)}. + * + * @author Chris Povirk + * @since 11.0 + */ +interface AsyncFunction<I, O> { + /** + * Returns an output {@code Future} to use in place of the given {@code + * input}. The output {@code Future} need not be {@linkplain Future#isDone + * done}, making {@code AsyncFunction} suitable for asynchronous derivations. + * <p> + * <p>Throwing an exception from this method is equivalent to returning a + * failing {@code Future}. + */ + ListenableFuture<O> apply(I input); +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/Cache.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/Cache.java new file mode 100644 index 0000000..b360b0a --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/Cache.java
@@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.concurrent.Callable; + +/** + * A semi-persistent mapping from keys to values. Cache entries are manually added using + * {@link #get(Object, Callable)} or {@link #put(Object, Object)}, and are stored in the cache until + * either evicted or manually invalidated. + * <p> + * <p>Implementations of this interface are expected to be thread-safe, and can be safely accessed + * by multiple concurrent threads. + * <p> + * <p>Note that while this class is still annotated as {@link Beta}, the API is frozen from a + * consumer's standpoint. In other words existing methods are all considered {@code non-Beta} and + * won't be changed without going through an 18 month deprecation cycle; however new methods may be + * added at any time. + * + * @author Charles Fry + * @since 10.0 + */ +public interface Cache<K, V> { + + /** + * Returns the value associated with {@code key} in this cache, or {@code null} if there is no + * cached value for {@code key}. + * + * @since 11.0 + */ + V getIfPresent(Object key); + + /** + * Associates {@code value} with {@code key} in this cache. If the cache previously contained a + * value associated with {@code key}, the old value is replaced by {@code value}. + * <p> + * <p>Prefer {@link #get(Object, Callable)} when using the conventional "if cached, return; + * otherwise create, cache and return" pattern. + * + * @since 11.0 + */ + void put(K key, V value); + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/CacheBuilder.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/CacheBuilder.java new file mode 100644 index 0000000..53b567e --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/CacheBuilder.java
@@ -0,0 +1,302 @@ +/* + * Copyright (C) 2009 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.ConcurrentModificationException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkArgument; +import static org.glassfish.jersey.internal.guava.Preconditions.checkState; + +/** + * <p>A builder of {@link LoadingCache} and {@link Cache} instances having any combination of the + * following features: + * <p> + * <ul> + * <li>automatic loading of entries into the cache + * <li>least-recently-used eviction when a maximum size is exceeded + * <li>time-based expiration of entries, measured since last access or last write + * <li>keys automatically wrapped in {@linkplain WeakReference weak} references + * <li>values automatically wrapped in {@linkplain WeakReference weak} or + * {@linkplain SoftReference soft} references + * <li>notification of evicted (or otherwise removed) entries + * <li>accumulation of cache access statistics + * </ul> + * <p> + * <p>These features are all optional; caches can be created using all or none of them. By default + * cache instances created by {@code CacheBuilder} will not perform any type of eviction. + * <p> + * <p>Usage example: <pre> {@code + * <p> + * LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() + * .maximumSize(10000) + * .expireAfterWrite(10, TimeUnit.MINUTES) + * .removalListener(MY_LISTENER) + * .build( + * new CacheLoader<Key, Graph>() { + * public Graph load(Key key) throws AnyException { + * return createExpensiveGraph(key); + * } + * });}</pre> + * <p> + * <p>Or equivalently, <pre> {@code + * <p> + * // In real life this would come from a command-line flag or config file + * String spec = "maximumSize=10000,expireAfterWrite=10m"; + * <p> + * LoadingCache<Key, Graph> graphs = CacheBuilder.from(spec) + * .removalListener(MY_LISTENER) + * .build( + * new CacheLoader<Key, Graph>() { + * public Graph load(Key key) throws AnyException { + * return createExpensiveGraph(key); + * } + * });}</pre> + * <p> + * <p>The returned cache is implemented as a hash table with similar performance characteristics to + * {@link ConcurrentHashMap}. It implements all optional operations of the {@link LoadingCache} and + * {@link Cache} interfaces. The {@code asMap} view (and its collection views) have <i>weakly + * consistent iterators</i>. This means that they are safe for concurrent use, but if other threads + * modify the cache after the iterator is created, it is undefined which of these changes, if any, + * are reflected in that iterator. These iterators never throw {@link + * ConcurrentModificationException}. + * <p> + * <p><b>Note:</b> by default, the returned cache uses equality comparisons (the + * {@link Object#equals equals} method) to determine equality for keys or values. However, if + * {@link #weakKeys} was specified, the cache uses identity ({@code ==}) + * comparisons instead for keys. Likewise, if {@link #weakValues} or {@link #softValues} was + * specified, the cache uses identity comparisons for values. + * <p> + * <p>Entries are automatically evicted from the cache when any of + * {@linkplain #maximumSize(long) maximumSize}, {@linkplain #maximumWeight(long) maximumWeight}, + * {@linkplain #expireAfterWrite expireAfterWrite}, + * {@linkplain #expireAfterAccess expireAfterAccess}, {@linkplain #weakKeys weakKeys}, + * {@linkplain #weakValues weakValues}, or {@linkplain #softValues softValues} are requested. + * <p> + * <p>If {@linkplain #maximumSize(long) maximumSize} or + * {@linkplain #maximumWeight(long) maximumWeight} is requested entries may be evicted on each cache + * modification. + * <p> + * <p>If {@linkplain #expireAfterWrite expireAfterWrite} or + * {@linkplain #expireAfterAccess expireAfterAccess} is requested entries may be evicted on each + * cache modification, on occasional cache accesses, or on calls to {@link Cache#cleanUp}. Expired + * entries may be counted by {@link Cache#size}, but will never be visible to read or write + * operations. + * <p> + * <p>If {@linkplain #weakKeys weakKeys}, {@linkplain #weakValues weakValues}, or + * {@linkplain #softValues softValues} are requested, it is possible for a key or value present in + * the cache to be reclaimed by the garbage collector. Entries with reclaimed keys or values may be + * removed from the cache on each cache modification, on occasional cache accesses, or on calls to + * {@link Cache#cleanUp}; such entries may be counted in {@link Cache#size}, but will never be + * visible to read or write operations. + * <p> + * <p>Certain cache configurations will result in the accrual of periodic maintenance tasks which + * will be performed during write operations, or during occasional read operations in the absence of + * writes. The {@link Cache#cleanUp} method of the returned cache will also perform maintenance, but + * calling it should not be necessary with a high throughput cache. Only caches built with + * {@linkplain #removalListener removalListener}, {@linkplain #expireAfterWrite expireAfterWrite}, + * {@linkplain #expireAfterAccess expireAfterAccess}, {@linkplain #weakKeys weakKeys}, + * {@linkplain #weakValues weakValues}, or {@linkplain #softValues softValues} perform periodic + * maintenance. + * <p> + * <p>The caches produced by {@code CacheBuilder} are serializable, and the deserialized caches + * retain all the configuration properties of the original cache. Note that the serialized form does + * <i>not</i> include cache contents, but only configuration. + * <p> + * <p>See the Guava User Guide article on <a href= + * "http://code.google.com/p/guava-libraries/wiki/CachesExplained">caching</a> for a higher-level + * explanation. + * + * @param <K> the base key type for all caches created by this builder + * @param <V> the base value type for all caches created by this builder + * @author Charles Fry + * @author Kevin Bourrillion + * @since 10.0 + */ +public final class CacheBuilder<K, V> { + + public static final Ticker NULL_TICKER = new Ticker() { + @Override + public long read() { + return 0; + } + }; + static final int UNSET_INT = -1; + static final int DEFAULT_INITIAL_CAPACITY = 16; + private static final int DEFAULT_CONCURRENCY_LEVEL = 4; + static final int DEFAULT_EXPIRATION_NANOS = 0; + static final int DEFAULT_REFRESH_NANOS = 0; + private final int initialCapacity = UNSET_INT; + private final int concurrencyLevel = UNSET_INT; + private long maximumSize = UNSET_INT; + private final long maximumWeight = UNSET_INT; + private final long expireAfterWriteNanos = UNSET_INT; + private long expireAfterAccessNanos = UNSET_INT; + private final long refreshNanos = UNSET_INT; + + // TODO(fry): make constructor private and update tests to use newBuilder + private CacheBuilder() { + } + + /** + * Constructs a new {@code CacheBuilder} instance with default settings, including strong keys, + * strong values, and no automatic eviction of any kind. + */ + public static CacheBuilder<Object, Object> newBuilder() { + return new CacheBuilder<Object, Object>(); + } + + int getConcurrencyLevel() { + return (concurrencyLevel == UNSET_INT) ? DEFAULT_CONCURRENCY_LEVEL : concurrencyLevel; + } + + /** + * Specifies the maximum number of entries the cache may contain. Note that the cache <b>may evict + * an entry before this limit is exceeded</b>. As the cache size grows close to the maximum, the + * cache evicts entries that are less likely to be used again. For example, the cache may evict an + * entry because it hasn't been used recently or very often. + * <p> + * <p>When {@code size} is zero, elements will be evicted immediately after being loaded into the + * cache. This can be useful in testing, or to disable caching temporarily without a code change. + * <p> + * <p>This feature cannot be used in conjunction with {@link #maximumWeight}. + * + * @param size the maximum size of the cache + * @throws IllegalArgumentException if {@code size} is negative + * @throws IllegalStateException if a maximum size or weight was already set + */ + public CacheBuilder<K, V> maximumSize(long size) { + checkState(this.maximumSize == UNSET_INT, "maximum size was already set to %s", + this.maximumSize); + checkState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s", + this.maximumWeight); + checkArgument(size >= 0, "maximum size must not be negative"); + this.maximumSize = size; + return this; + } + + /** + * Specifies that each entry should be automatically removed from the cache once a fixed duration + * has elapsed after the entry's creation, the most recent replacement of its value, or its last + * access. Access time is reset by all cache read and write operations (including + * {@code Cache.asMap().get(Object)} and {@code Cache.asMap().put(K, V)}), but not by operations + * on the collection-views of {@link Cache#asMap}. + * <p> + * <p>When {@code duration} is zero, this method hands off to + * {@link #maximumSize(long) maximumSize}{@code (0)}, ignoring any otherwise-specificed maximum + * size or weight. This can be useful in testing, or to disable caching temporarily without a code + * change. + * <p> + * <p>Expired entries may be counted in {@link Cache#size}, but will never be visible to read or + * write operations. Expired entries are cleaned up as part of the routine maintenance described + * in the class javadoc. + * + * @param duration the length of time after an entry is last accessed that it should be + * automatically removed + * @param unit the unit that {@code duration} is expressed in + * @throws IllegalArgumentException if {@code duration} is negative + * @throws IllegalStateException if the time to idle or time to live was already set + */ + public CacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) { + checkState(expireAfterAccessNanos == UNSET_INT, "expireAfterAccess was already set to %s ns", + expireAfterAccessNanos); + checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); + this.expireAfterAccessNanos = unit.toNanos(duration); + return this; + } + + long getExpireAfterAccessNanos() { + return (expireAfterAccessNanos == UNSET_INT) + ? DEFAULT_EXPIRATION_NANOS : expireAfterAccessNanos; + } + + /** + * Builds a cache, which either returns an already-loaded value for a given key or atomically + * computes or retrieves it using the supplied {@code CacheLoader}. If another thread is currently + * loading the value for this key, simply waits for that thread to finish and returns its + * loaded value. Note that multiple threads can concurrently load values for distinct keys. + * <p> + * <p>This method does not alter the state of this {@code CacheBuilder} instance, so it can be + * invoked again to create multiple independent caches. + * + * @param loader the cache loader used to obtain new values + * @return a cache having the requested features + */ + public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build( + CacheLoader<? super K1, V1> loader) { + checkWeightWithWeigher(); + return new LocalCache.LocalLoadingCache<K1, V1>(this, loader); + } + + /** + * Builds a cache which does not automatically load values when keys are requested. + * <p> + * <p>Consider {@link #build(CacheLoader)} instead, if it is feasible to implement a + * {@code CacheLoader}. + * <p> + * <p>This method does not alter the state of this {@code CacheBuilder} instance, so it can be + * invoked again to create multiple independent caches. + * + * @return a cache having the requested features + * @since 11.0 + */ + public <K1 extends K, V1 extends V> Cache<K1, V1> build() { + checkWeightWithWeigher(); + checkNonLoadingCache(); + return new LocalCache.LocalManualCache<K1, V1>(this); + } + + private void checkNonLoadingCache() { + checkState(refreshNanos == UNSET_INT, "refreshAfterWrite requires a LoadingCache"); + } + + private void checkWeightWithWeigher() { + checkState(maximumWeight == UNSET_INT, "maximumWeight requires weigher"); + } + + /** + * Returns a string representation for this CacheBuilder instance. The exact form of the returned + * string is not specified. + */ + @Override + public String toString() { + MoreObjects.ToStringHelper s = MoreObjects.toStringHelper(this); + if (initialCapacity != UNSET_INT) { + s.add("initialCapacity", initialCapacity); + } + if (concurrencyLevel != UNSET_INT) { + s.add("concurrencyLevel", concurrencyLevel); + } + if (maximumSize != UNSET_INT) { + s.add("maximumSize", maximumSize); + } + if (maximumWeight != UNSET_INT) { + s.add("maximumWeight", maximumWeight); + } + if (expireAfterWriteNanos != UNSET_INT) { + s.add("expireAfterWrite", expireAfterWriteNanos + "ns"); + } + if (expireAfterAccessNanos != UNSET_INT) { + s.add("expireAfterAccess", expireAfterAccessNanos + "ns"); + } + return s.toString(); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/CacheLoader.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/CacheLoader.java new file mode 100644 index 0000000..cd8859b --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/CacheLoader.java
@@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; + +/** + * Computes or retrieves values, based on a key, for use in populating a {@link LoadingCache}. + * <p> + * <p>Most implementations will only need to implement {@link #load}. Other methods may be + * overridden as desired. + * <p> + * <p>Usage example: <pre> {@code + * <p> + * CacheLoader<Key, Graph> loader = new CacheLoader<Key, Graph>() { + * public Graph load(Key key) throws AnyException { + * return createExpensiveGraph(key); + * } + * }; + * LoadingCache<Key, Graph> cache = CacheBuilder.newBuilder().build(loader);}</pre> + * + * @author Charles Fry + * @since 10.0 + */ +public abstract class CacheLoader<K, V> { + /** + * Constructor for use by subclasses. + */ + protected CacheLoader() { + } + + /** + * Computes or retrieves the value corresponding to {@code key}. + * + * @param key the non-null key whose value should be loaded + * @return the value associated with {@code key}; <b>must not be null</b> + * @throws Exception if unable to load the result + * @throws InterruptedException if this method is interrupted. {@code InterruptedException} is + * treated like any other {@code Exception} in all respects except that, when it is caught, + * the thread's interrupt status is set + */ + public abstract V load(K key) throws Exception; + + /** + * Computes or retrieves a replacement value corresponding to an already-cached {@code key}. This + * method is called when an existing cache entry is refreshed by + * {@link CacheBuilder#refreshAfterWrite}, or through a call to {@link LoadingCache#refresh}. + * <p> + * <p>This implementation synchronously delegates to {@link #load}. It is recommended that it be + * overridden with an asynchronous implementation when using + * {@link CacheBuilder#refreshAfterWrite}. + * <p> + * <p><b>Note:</b> <i>all exceptions thrown by this method will be logged and then swallowed</i>. + * + * @param key the non-null key whose value should be loaded + * @param oldValue the non-null old value corresponding to {@code key} + * @return the future new value associated with {@code key}; + * <b>must not be null, must not return null</b> + * @throws Exception if unable to reload the result + * @throws InterruptedException if this method is interrupted. {@code InterruptedException} is + * treated like any other {@code Exception} in all respects except that, when it is caught, + * the thread's interrupt status is set + * @since 11.0 + */ + public ListenableFuture<V> reload(K key, V oldValue) throws Exception { + checkNotNull(key); + checkNotNull(oldValue); + return Futures.immediateFuture(load(key)); + } + + /** + * Thrown to indicate that an invalid response was returned from a call to {@link CacheLoader}. + * + * @since 11.0 + */ + public static final class InvalidCacheLoadException extends RuntimeException { + public InvalidCacheLoadException(String message) { + super(message); + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/CollectPreconditions.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/CollectPreconditions.java new file mode 100644 index 0000000..ad07373 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/CollectPreconditions.java
@@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkState; + +/** + * Precondition checks useful in collection implementations. + */ +final class CollectPreconditions { + + static int checkNonnegative(int value, String name) { + if (value < 0) { + throw new IllegalArgumentException(name + " cannot be negative but was: " + value); + } + return value; + } + + /** + * Precondition tester for {@code Iterator.remove()} that throws an exception with a consistent + * error message. + */ + static void checkRemove(boolean canRemove) { + checkState(canRemove, "no calls to next() since the last call to remove()"); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/Collections2.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/Collections2.java new file mode 100644 index 0000000..d081b06 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/Collections2.java
@@ -0,0 +1,144 @@ +/* + * Copyright (C) 2008 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.Function; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkArgument; +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; + +/** + * Provides static methods for working with {@code Collection} instances. + * + * @author Chris Povirk + * @author Mike Bostock + * @author Jared Levy + * @since 2.0 (imported from Google Collections Library) + */ +final class Collections2 { + static final Joiner STANDARD_JOINER = Joiner.on(); + + private Collections2() { + } + + /** + * Delegates to {@link Collection#contains}. Returns {@code false} if the + * {@code contains} method throws a {@code ClassCastException} or + * {@code NullPointerException}. + */ + static boolean safeContains( + Collection<?> collection, Object object) { + checkNotNull(collection); + try { + return collection.contains(object); + } catch (ClassCastException e) { + return false; + } catch (NullPointerException e) { + return false; + } + } + + /** + * Delegates to {@link Collection#remove}. Returns {@code false} if the + * {@code remove} method throws a {@code ClassCastException} or + * {@code NullPointerException}. + */ + static boolean safeRemove(Collection<?> collection, Object object) { + checkNotNull(collection); + try { + return collection.remove(object); + } catch (ClassCastException e) { + return false; + } catch (NullPointerException e) { + return false; + } + } + + /** + * Returns a collection that applies {@code function} to each element of + * {@code fromCollection}. The returned collection is a live view of {@code + * fromCollection}; changes to one affect the other. + * <p> + * <p>The returned collection's {@code add()} and {@code addAll()} methods + * throw an {@link UnsupportedOperationException}. All other collection + * methods are supported, as long as {@code fromCollection} supports them. + * <p> + * <p>The returned collection isn't threadsafe or serializable, even if + * {@code fromCollection} is. + * <p> + * <p>When a live view is <i>not</i> needed, it may be faster to copy the + * transformed collection and use the copy. + * <p> + * <p>If the input {@code Collection} is known to be a {@code List}, consider + * {@link Lists#transform}. If only an {@code Iterable} is available, use + * {@link Iterables#transform}. + */ + public static <F, T> Collection<T> transform(Collection<F> fromCollection, + Function<? super F, T> function) { + return new TransformedCollection<F, T>(fromCollection, function); + } + + /** + * Returns best-effort-sized StringBuilder based on the given collection size. + */ + static StringBuilder newStringBuilderForCollection(int size) { + CollectPreconditions.checkNonnegative(size, "size"); + return new StringBuilder((int) Math.min(size * 8L, Ints.MAX_POWER_OF_TWO)); + } + + /** + * Used to avoid http://bugs.sun.com/view_bug.do?bug_id=6558557 + */ + static <T> Collection<T> cast(Iterable<T> iterable) { + return (Collection<T>) iterable; + } + + static class TransformedCollection<F, T> extends AbstractCollection<T> { + final Collection<F> fromCollection; + final Function<? super F, ? extends T> function; + + TransformedCollection(Collection<F> fromCollection, + Function<? super F, ? extends T> function) { + this.fromCollection = checkNotNull(fromCollection); + this.function = checkNotNull(function); + } + + @Override + public void clear() { + fromCollection.clear(); + } + + @Override + public boolean isEmpty() { + return fromCollection.isEmpty(); + } + + @Override + public Iterator<T> iterator() { + return Iterators.transform(fromCollection.iterator(), function); + } + + @Override + public int size() { + return fromCollection.size(); + } + } + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/ComparatorOrdering.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ComparatorOrdering.java new file mode 100644 index 0000000..1cba96a --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ComparatorOrdering.java
@@ -0,0 +1,61 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.io.Serializable; +import java.util.Comparator; + +import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; + +/** + * An ordering for a pre-existing comparator. + */ +final class ComparatorOrdering<T> extends Ordering<T> implements Serializable { + private static final long serialVersionUID = 0; + private final Comparator<T> comparator; + + ComparatorOrdering(Comparator<T> comparator) { + this.comparator = checkNotNull(comparator); + } + + @Override + public int compare(T a, T b) { + return comparator.compare(a, b); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object instanceof ComparatorOrdering) { + ComparatorOrdering<?> that = (ComparatorOrdering<?>) object; + return this.comparator.equals(that.comparator); + } + return false; + } + + @Override + public int hashCode() { + return comparator.hashCode(); + } + + @Override + public String toString() { + return comparator.toString(); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/Equivalence.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/Equivalence.java new file mode 100644 index 0000000..6a1b4e0 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/Equivalence.java
@@ -0,0 +1,166 @@ +/* + * Copyright (C) 2010 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.io.Serializable; + +/** + * A strategy for determining whether two instances are considered equivalent. Examples of + * equivalences are the {@linkplain #identity() identity equivalence} and {@linkplain #equals equals + * equivalence}. + * + * @author Bob Lee + * @author Ben Yu + * @author Gregory Kick + * @since 10.0 (<a href="http://code.google.com/p/guava-libraries/wiki/Compatibility" + * >mostly source-compatible</a> since 4.0) + */ +public abstract class Equivalence<T> { + + /** + * Returns an equivalence that delegates to {@link Object#equals} and {@link Object#hashCode}. + * {@link Equivalence#equivalent} returns {@code true} if both values are null, or if neither + * value is null and {@link Object#equals} returns {@code true}. {@link Equivalence#hash} returns + * {@code 0} if passed a null value. + * + * @since 4.0 (in Equivalences) + */ + public static Equivalence<Object> equals() { + return Equals.INSTANCE; + } + + /** + * Returns an equivalence that uses {@code ==} to compare values and {@link + * System#identityHashCode(Object)} to compute the hash code. {@link Equivalence#equivalent} + * returns {@code true} if {@code a == b}, including in the case that a and b are both null. + * + * @since 4.0 (in Equivalences) + */ + public static Equivalence<Object> identity() { + return Identity.INSTANCE; + } + + /** + * Returns {@code true} if the given objects are considered equivalent. + * <p> + * <p>The {@code equivalent} method implements an equivalence relation on object references: + * <p> + * <ul> + * <li>It is <i>reflexive</i>: for any reference {@code x}, including null, {@code + * equivalent(x, x)} returns {@code true}. + * <li>It is <i>symmetric</i>: for any references {@code x} and {@code y}, {@code + * equivalent(x, y) == equivalent(y, x)}. + * <li>It is <i>transitive</i>: for any references {@code x}, {@code y}, and {@code z}, if + * {@code equivalent(x, y)} returns {@code true} and {@code equivalent(y, z)} returns {@code + * true}, then {@code equivalent(x, z)} returns {@code true}. + * <li>It is <i>consistent</i>: for any references {@code x} and {@code y}, multiple invocations + * of {@code equivalent(x, y)} consistently return {@code true} or consistently return {@code + * false} (provided that neither {@code x} nor {@code y} is modified). + * </ul> + */ + public final boolean equivalent(T a, T b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return doEquivalent(a, b); + } + + /** + * Returns {@code true} if {@code a} and {@code b} are considered equivalent. + * <p> + * <p>Called by {@link #equivalent}. {@code a} and {@code b} are not the same + * object and are not nulls. + * + * @since 10.0 (previously, subclasses would override equivalent()) + */ + protected abstract boolean doEquivalent(T a, T b); + + /** + * Returns a hash code for {@code t}. + * <p> + * <p>The {@code hash} has the following properties: + * <ul> + * <li>It is <i>consistent</i>: for any reference {@code x}, multiple invocations of + * {@code hash(x}} consistently return the same value provided {@code x} remains unchanged + * according to the definition of the equivalence. The hash need not remain consistent from + * one execution of an application to another execution of the same application. + * <li>It is <i>distributable across equivalence</i>: for any references {@code x} and {@code y}, + * if {@code equivalent(x, y)}, then {@code hash(x) == hash(y)}. It is <i>not</i> necessary + * that the hash be distributable across <i>inequivalence</i>. If {@code equivalence(x, y)} + * is false, {@code hash(x) == hash(y)} may still be true. + * <li>{@code hash(null)} is {@code 0}. + * </ul> + */ + public final int hash(T t) { + if (t == null) { + return 0; + } + return doHash(t); + } + + /** + * Returns a hash code for non-null object {@code t}. + * <p> + * <p>Called by {@link #hash}. + * + * @since 10.0 (previously, subclasses would override hash()) + */ + protected abstract int doHash(T t); + + static final class Equals extends Equivalence<Object> implements Serializable { + + static final Equivalence.Equals INSTANCE = new Equivalence.Equals(); + private static final long serialVersionUID = 1; + + @Override + protected boolean doEquivalent(Object a, Object b) { + return a.equals(b); + } + + @Override + protected int doHash(Object o) { + return o.hashCode(); + } + + private Object readResolve() { + return INSTANCE; + } + } + + static final class Identity extends Equivalence<Object> implements Serializable { + + static final Equivalence.Identity INSTANCE = new Equivalence.Identity(); + private static final long serialVersionUID = 1; + + @Override + protected boolean doEquivalent(Object a, Object b) { + return false; + } + + @Override + protected int doHash(Object o) { + return System.identityHashCode(o); + } + + private Object readResolve() { + return INSTANCE; + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/ExecutionError.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ExecutionError.java new file mode 100644 index 0000000..b0c0be8 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ExecutionError.java
@@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +/** + * {@link Error} variant of {@link java.util.concurrent.ExecutionException}. As + * with {@code ExecutionException}, the error's {@linkplain #getCause() cause} + * comes from a failed task, possibly run in another thread. That cause should + * itself be an {@code Error}; if not, use {@code ExecutionException} or {@link + * UncheckedExecutionException}. This allows the client code to continue to + * distinguish between exceptions and errors, even when they come from other + * threads. + * + * @author Chris Povirk + * @since 10.0 + */ +public class ExecutionError extends Error { + + private static final long serialVersionUID = 0; + + /** + * Creates a new instance with the given cause. + */ + public ExecutionError(Error cause) { + super(cause); + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/ExecutionList.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ExecutionList.java new file mode 100644 index 0000000..1ba2b73 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ExecutionList.java
@@ -0,0 +1,172 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * <p>A list of listeners, each with an associated {@code Executor}, that + * guarantees that every {@code Runnable} that is {@linkplain #add added} will + * be executed after {@link #execute()} is called. Any {@code Runnable} added + * after the call to {@code execute} is still guaranteed to execute. There is no + * guarantee, however, that listeners will be executed in the order that they + * are added. + * <p> + * <p>Exceptions thrown by a listener will be propagated up to the executor. + * Any exception thrown during {@code Executor.execute} (e.g., a {@code + * RejectedExecutionException} or an exception thrown by {@linkplain + * MoreExecutors#directExecutor direct execution}) will be caught and + * logged. + * + * @author Nishant Thakkar + * @author Sven Mawson + * @since 1.0 + */ +final class ExecutionList { + // Logger to log exceptions caught when running runnables. + private static final Logger log = Logger.getLogger(ExecutionList.class.getName()); + + /** + * The runnable, executor pairs to execute. This acts as a stack threaded through the + * {@link RunnableExecutorPair#next} field. + */ + private RunnableExecutorPair runnables; + private boolean executed; + + /** + * Creates a new, empty {@link ExecutionList}. + */ + public ExecutionList() { + } + + /** + * Submits the given runnable to the given {@link Executor} catching and logging all + * {@linkplain RuntimeException runtime exceptions} thrown by the executor. + */ + private static void executeListener(Runnable runnable, Executor executor) { + try { + executor.execute(runnable); + } catch (RuntimeException e) { + // Log it and keep going, bad runnable and/or executor. Don't + // punish the other runnables if we're given a bad one. We only + // catch RuntimeException because we want Errors to propagate up. + log.log(Level.SEVERE, "RuntimeException while executing runnable " + + runnable + " with executor " + executor, e); + } + } + + /** + * Adds the {@code Runnable} and accompanying {@code Executor} to the list of + * listeners to execute. If execution has already begun, the listener is + * executed immediately. + * <p> + * <p>Note: For fast, lightweight listeners that would be safe to execute in + * any thread, consider {@link MoreExecutors#directExecutor}. For heavier + * listeners, {@code directExecutor()} carries some caveats: First, the + * thread that the listener runs in depends on whether the {@code + * ExecutionList} has been executed at the time it is added. In particular, + * listeners may run in the thread that calls {@code add}. Second, the thread + * that calls {@link #execute} may be an internal implementation thread, such + * as an RPC network thread, and {@code directExecutor()} listeners may + * run in this thread. Finally, during the execution of a {@code + * directExecutor} listener, all other registered but unexecuted + * listeners are prevented from running, even if those listeners are to run + * in other executors. + */ + public void add(Runnable runnable, Executor executor) { + // Fail fast on a null. We throw NPE here because the contract of + // Executor states that it throws NPE on null listener, so we propagate + // that contract up into the add method as well. + Preconditions.checkNotNull(runnable, "Runnable was null."); + Preconditions.checkNotNull(executor, "Executor was null."); + + // Lock while we check state. We must maintain the lock while adding the + // new pair so that another thread can't run the list out from under us. + // We only add to the list if we have not yet started execution. + synchronized (this) { + if (!executed) { + runnables = new RunnableExecutorPair(runnable, executor, runnables); + return; + } + } + // Execute the runnable immediately. Because of scheduling this may end up + // getting called before some of the previously added runnables, but we're + // OK with that. If we want to change the contract to guarantee ordering + // among runnables we'd have to modify the logic here to allow it. + executeListener(runnable, executor); + } + + /** + * Runs this execution list, executing all existing pairs in the order they + * were added. However, note that listeners added after this point may be + * executed before those previously added, and note that the execution order + * of all listeners is ultimately chosen by the implementations of the + * supplied executors. + * <p> + * <p>This method is idempotent. Calling it several times in parallel is + * semantically equivalent to calling it exactly once. + * + * @since 10.0 (present in 1.0 as {@code run}) + */ + public void execute() { + // Lock while we update our state so the add method above will finish adding + // any listeners before we start to run them. + RunnableExecutorPair list; + synchronized (this) { + if (executed) { + return; + } + executed = true; + list = runnables; + runnables = null; // allow GC to free listeners even if this stays around for a while. + } + // If we succeeded then list holds all the runnables we to execute. The pairs in the stack are + // in the opposite order from how they were added so we need to reverse the list to fulfill our + // contract. + // This is somewhat annoying, but turns out to be very fast in practice. Alternatively, we + // could drop the contract on the method that enforces this queue like behavior since depending + // on it is likely to be a bug anyway. + + // N.B. All writes to the list and the next pointers must have happened before the above + // synchronized block, so we can iterate the list without the lock held here. + RunnableExecutorPair reversedList = null; + while (list != null) { + RunnableExecutorPair tmp = list; + list = list.next; + tmp.next = reversedList; + reversedList = tmp; + } + while (reversedList != null) { + executeListener(reversedList.runnable, reversedList.executor); + reversedList = reversedList.next; + } + } + + private static final class RunnableExecutorPair { + final Runnable runnable; + final Executor executor; + RunnableExecutorPair next; + + RunnableExecutorPair(Runnable runnable, Executor executor, RunnableExecutorPair next) { + this.runnable = runnable; + this.executor = executor; + this.next = next; + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/ForwardingCollection.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ForwardingCollection.java new file mode 100644 index 0000000..f00dff2 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ForwardingCollection.java
@@ -0,0 +1,120 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.glassfish.jersey.internal.guava; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A collection which forwards all its method calls to another collection. + * Subclasses should override one or more methods to modify the behavior of the + * backing collection as desired per the <a + * href="http://en.wikipedia.org/wiki/Decorator_pattern">decorator pattern</a>. + * <p> + * <p><b>Warning:</b> The methods of {@code ForwardingCollection} forward + * <b>indiscriminately</b> to the methods of the delegate. For example, + * overriding {@link #add} alone <b>will not</b> change the behavior of {@link + * #addAll}, which can lead to unexpected behavior. In this case, you should + * override {@code addAll} as well, either providing your own implementation, or + * delegating to the provided {@code standardAddAll} method. + * <p> + * <p>The {@code standard} methods are not guaranteed to be thread-safe, even + * when all of the methods that they depend on are thread-safe. + * + * @author Kevin Bourrillion + * @author Louis Wasserman + * @since 2.0 (imported from Google Collections Library) + */ +public abstract class ForwardingCollection<E> extends ForwardingObject + implements Collection<E> { + // TODO(user): identify places where thread safety is actually lost + + /** + * Constructor for use by subclasses. + */ + ForwardingCollection() { + } + + @Override + protected abstract Collection<E> delegate(); + + @Override + public Iterator<E> iterator() { + return delegate().iterator(); + } + + @Override + public int size() { + return delegate().size(); + } + + @Override + public boolean removeAll(Collection<?> collection) { + return delegate().removeAll(collection); + } + + @Override + public boolean isEmpty() { + return delegate().isEmpty(); + } + + @Override + public boolean contains(Object object) { + return delegate().contains(object); + } + + @Override + public boolean add(E element) { + return delegate().add(element); + } + + @Override + public boolean remove(Object object) { + return delegate().remove(object); + } + + @Override + public boolean containsAll(Collection<?> collection) { + return delegate().containsAll(collection); + } + + @Override + public boolean addAll(Collection<? extends E> collection) { + return delegate().addAll(collection); + } + + @Override + public boolean retainAll(Collection<?> collection) { + return delegate().retainAll(collection); + } + + @Override + public void clear() { + delegate().clear(); + } + + @Override + public Object[] toArray() { + return delegate().toArray(); + } + + @Override + public <T> T[] toArray(T[] array) { + return delegate().toArray(array); + } + +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/guava/ForwardingMapEntry.java b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ForwardingMapEntry.java new file mode 100644 index 0000000..3f6b1ad --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/guava/ForwardingMapEntry.java
@@ -0,0 +1,103 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * l