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 &#64;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 &#64;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
+ * &#64;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 &#64;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 &#64;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 &gt;&nbsp;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 &gt;&nbsp;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&lt;T&gt;) 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&lt;&gt() {...}}.
+     * </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&lt;T&gt;) 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&lt;T&gt;) 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 {
+ *         &#64;Inject
+ *         MyInjectedService service;
+ *
+ *         &#64;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 {
+ *
+ *         &#64;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