Index: SRUClient/tags/SRUClient-0.9.2/LICENSE.txt
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/LICENSE.txt	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/LICENSE.txt	(revision 2998)
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
Index: SRUClient/tags/SRUClient-0.9.2/pom.xml
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/pom.xml	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/pom.xml	(revision 2998)
@@ -0,0 +1,148 @@
+<?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>
+    <groupId>eu.clarin.sru</groupId>
+    <artifactId>sru-client</artifactId>
+    <version>0.9.2</version>
+    <name>SRU/CQL Client</name>
+    <description>A SRU/CQL client implementation conforming to SRU/CQL protocol version 1.1 and 1.2	</description>
+    <organization>
+        <name>IDS Mannheim</name>
+        <url>http://www.ids-mannheim.de/</url>
+    </organization>
+
+    <properties>
+        <!-- project settings -->
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <!-- plug-in settings -->
+        <maven.compiler.source>1.6</maven.compiler.source>
+        <maven.compiler.target>1.6</maven.compiler.target>
+        <!-- versions of common dependencies -->
+        <slf4j.version>1.7.2</slf4j.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.2.5</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.woodstox</groupId>
+            <artifactId>woodstox-core-lgpl</artifactId>
+            <version>4.2.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <!-- <artifactId>slf4j-jdk14</artifactId> -->
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.1</version>
+                <configuration>
+                    <source>${maven.compiler.source}</source>
+                    <target>${maven.compiler.target}</target>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.8.1</version>
+                <executions>
+                    <execution>
+                        <id>attach-javadoc</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.2</version>
+                <executions>
+                    <execution>
+                        <id>attach-source</id>
+                        <goals>
+                            <goal>jar-no-fork</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <version>2.5.1</version>
+                <executions>
+                    <execution>
+                        <id>copy-dependencies</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <includeScope>compile</includeScope>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>false</overWriteSnapshots>
+                            <overWriteIfNewer>true</overWriteIfNewer>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+
+        <pluginManagement>
+            <plugins>
+                <!-- m2e settings -->
+                <plugin>
+                    <groupId>org.eclipse.m2e</groupId>
+                    <artifactId>lifecycle-mapping</artifactId>
+                    <version>1.0.0</version>
+                    <configuration>
+                        <lifecycleMappingMetadata>
+                            <pluginExecutions>
+                                <!-- copy-dependency plugin -->
+                                <pluginExecution>
+                                    <pluginExecutionFilter>
+                                        <groupId>org.apache.maven.plugins</groupId>
+                                        <artifactId>maven-dependency-plugin</artifactId>
+                                        <versionRange>[1.0.0,)</versionRange>
+                                        <goals>
+                                            <goal>copy-dependencies</goal>
+                                        </goals>
+                                    </pluginExecutionFilter>
+                                    <action>
+                                        <ignore />
+                                    </action>
+                                </pluginExecution>
+                            </pluginExecutions>
+                        </lifecycleMappingMetadata>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+</project>
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUAbstractRequest.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUAbstractRequest.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUAbstractRequest.java	(revision 2998)
@@ -0,0 +1,402 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.http.client.utils.URIBuilder;
+
+
+
+/**
+ * Abstract base class for SRU requests.
+ *
+ * @see SRUExplainResponse
+ * @see SRUScanResponse
+ * @see SRUSearchRetrieveResponse
+ */
+abstract class SRUAbstractRequest {
+    static final String PARAM_OPERATION                = "operation";
+    static final String PARAM_VERSION                  = "version";
+    static final String PARAM_RECORD_PACKING           = "recordPacking";
+    static final String PARAM_STYLESHEET               = "stylesheet";
+    static final String PARAM_QUERY                    = "query";
+    static final String PARAM_START_RECORD             = "startRecord";
+    static final String PARAM_MAXIMUM_RECORDS          = "maximumRecords";
+    static final String PARAM_RECORD_SCHEMA            = "recordSchema";
+    static final String PARAM_RECORD_X_PATH            = "recordXPath";
+    static final String PARAM_RESULT_SET_TTL           = "resultSetTTL";
+    static final String PARAM_SORT_KEYS                = "sortKeys";
+    static final String PARAM_SCAN_CLAUSE              = "scanClause";
+    static final String PARAM_RESPONSE_POSITION        = "responsePosition";
+    static final String PARAM_MAXIMUM_TERMS            = "maximumTerms";
+    static final String RECORD_PACKING_XML             = "xml";
+    static final String RECORD_PACKING_STRING          = "string";
+    private static final String OP_EXPLAIN             = "explain";
+    private static final String OP_SCAN                = "scan";
+    private static final String OP_SEARCH_RETRIEVE     = "searchRetrieve";
+    private static final String VERSION_1_1            = "1.1";
+    private static final String VERSION_1_2            = "1.2";
+    private static final String PARAM_EXTENSION_PREFIX = "x-";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_OPERATION   =
+            "x-malformed-operation";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_VERSION     =
+            "x-malformed-version";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String MALFORMED_OMIT          = "omit";
+    private static final String MALFORMED_KEY_PREFIX   = "x-malformed";
+
+
+    enum SRUOperation {
+        EXPLAIN, SCAN, SEARCH_RETRIEVE
+    } // enum SRUOperation
+
+
+    static class URIHelper {
+        private final URIBuilder uriBuilder;
+
+
+        private URIHelper(URIBuilder builder) {
+            this.uriBuilder = builder;
+        }
+
+
+        public URIHelper append(String name, String value) {
+            if (name == null) {
+                throw new NullPointerException("name == null");
+            }
+            if (name.isEmpty()) {
+                throw new IllegalArgumentException("name is empty");
+            }
+            if (value == null) {
+                throw new NullPointerException("value == null");
+            }
+            if (value.isEmpty()) {
+                throw new IllegalArgumentException("value is empty");
+            }
+
+            uriBuilder.addParameter(name, value);
+            return this;
+        }
+
+
+        public URIHelper append(String name, int value) {
+            return append(name, Integer.toString(value));
+        }
+
+
+        private URI makeURI() throws URISyntaxException {
+            return uriBuilder.build();
+        }
+    } // class URIHelper
+    /** The baseURI of the SRU server. */
+    protected final URI baseURI;
+    /**
+     * The request should be processed in strict or non-strict SRU protocol
+     * conformance mode. Default value is <code>true</code>.
+     */
+    private boolean strictMode = true;
+    /** The version to be used for this request. */
+    protected SRUVersion version;
+    /** A map of extra request data parameters. */
+    protected Map<String, String> extraRequestData;
+    private SRUVersion versionPerformed;
+
+
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the SRU server
+     * @throws NullPointerException
+     *             if any required argument is null
+     */
+    protected SRUAbstractRequest(URI baseURI) {
+        if (baseURI == null) {
+            throw new NullPointerException("baseURI == null");
+        }
+        this.baseURI = baseURI;
+    }
+
+
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the SRU server
+     * @throws NullPointerException
+     *             if any required argument is null
+     * @throw IllegalArgumentException if the URI is invalid
+     */
+    protected SRUAbstractRequest(String baseURI) {
+        if (baseURI == null) {
+            throw new NullPointerException("baseURI == null");
+        }
+        if (baseURI.isEmpty()) {
+            throw new IllegalArgumentException("baseURI is empty");
+        }
+        try {
+            this.baseURI = new URI(baseURI);
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("invalid URI", e);
+        }
+    }
+
+
+    /**
+     * Get the baseURI of the SRU server.
+     *
+     * @return the baseURI of the SRU server
+     */
+    public URI getBaseURI() {
+        return baseURI;
+    }
+
+
+    /**
+     * Get the SRU protocol conformance mode for this request
+     *
+     * @return <code>true</code> if the request will be performed in strict
+     *         mode, <code>false</code> if the request will be performed in a
+     *         more tolerant mode
+     */
+    public boolean isStrictMode() {
+        return strictMode;
+    }
+
+
+    /**
+     * Set the SRU protocol conformance mode for this request
+     *
+     * @param strictMode
+     *            <code>true</code> if the request should be performed in strict
+     *            mode, <code>false</code> if the request should be performed
+     *            client should in a more tolerant mode
+     */
+    public void setStrictMode(boolean strictMode) {
+        this.strictMode = strictMode;
+    }
+
+
+    /**
+     * Set the version for this request.
+     *
+     * @param version a version of <code>null</code> for client default
+     */
+    public void setVersion(SRUVersion version) {
+        this.version = version;
+    }
+
+
+    /**
+     * Get the version for this request.
+     *
+     * @return version for this request or <code>null</code> of client default
+     *         is used
+     */
+    public SRUVersion getVersion() {
+        return version;
+    }
+
+
+    /**
+     * Set an extra request parameter for this request.
+     *
+     * @param name
+     *            the name for the extra request data parameter
+     * @param value
+     *            the value for the extra request data parameter
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     * @see <a href="http://www.loc.gov/standards/sru/specs/extra-data.html">SRU
+     *      Extra Data / Extensions</a>
+     */
+    public void setExtraRequestData(String name, String value) {
+        if (name == null) {
+            throw new NullPointerException("name == null");
+        }
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("name is an empty string");
+        }
+        if (!name.startsWith(PARAM_EXTENSION_PREFIX)) {
+            throw new IllegalArgumentException("name must start with '" +
+                    PARAM_EXTENSION_PREFIX + "'");
+        }
+        if (value == null) {
+            throw new NullPointerException("value == null");
+        }
+        if (value.isEmpty()) {
+            throw new IllegalArgumentException("value is an empty string");
+        }
+        if (extraRequestData == null) {
+            extraRequestData = new HashMap<String, String>();
+        }
+        extraRequestData.put(name, value);
+    }
+
+
+    /**
+     * Set the value of extra request parameter for this request.
+     *
+     * @param name
+     *            the name for the extra request data parameter
+     * @return the value for the extra request data parameter or
+     *         <code>null</code> if parameter was not set
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     * @see <a href="http://www.loc.gov/standards/sru/specs/extra-data.html">SRU
+     *      Extra Data / Extensions</a>
+     */
+    public String getExtraRequestData(String name) {
+        if (name == null) {
+            throw new NullPointerException("name == null");
+        }
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("name is an empty string");
+        }
+        if (!name.startsWith(PARAM_EXTENSION_PREFIX)) {
+            throw new IllegalArgumentException("name must start with '" +
+                    PARAM_EXTENSION_PREFIX + "'");
+        }
+        if (extraRequestData != null) {
+            return extraRequestData.get(name);
+        }
+        return null;
+    }
+
+
+    final SRUVersion getVersionPerformed() {
+        return versionPerformed;
+    }
+
+
+    final URI makeURI(SRUVersion defaultVersion)
+            throws SRUClientException {
+        if (defaultVersion == null) {
+            throw new NullPointerException("defaultVersion == null");
+        }
+
+        try {
+            final URIHelper uriBuilder =
+                    new URIHelper(new URIBuilder(baseURI));
+
+            /*
+             * append operation parameter
+             *
+             * NB: Setting "x-malformed-operation" as an extra request parameter
+             * makes the client to send invalid requests. This is intended to
+             * use for testing SRU servers for protocol conformance (i.e.
+             * provoke an error) and SHOULD NEVER be used in production!
+             */
+            final String malformedOperation =
+                    getExtraRequestData(X_MALFORMED_OPERATION);
+            if (malformedOperation == null) {
+                switch (getOperation()) {
+                case EXPLAIN:
+                    uriBuilder.append(PARAM_OPERATION, OP_EXPLAIN);
+                    break;
+                case SCAN:
+                    uriBuilder.append(PARAM_OPERATION, OP_SCAN);
+                    break;
+                case SEARCH_RETRIEVE:
+                    uriBuilder.append(PARAM_OPERATION, OP_SEARCH_RETRIEVE);
+                    break;
+                default:
+                    throw new SRUClientException("unsupported operation: " +
+                            getOperation());
+                } // switch
+            } else {
+                if (!malformedOperation.equals(MALFORMED_OMIT)) {
+                    uriBuilder.append(PARAM_OPERATION, malformedOperation);
+                }
+            }
+
+            /*
+             * append version parameter
+             *
+             * NB: Setting "x-malformed-version" as an extra request parameter
+             * makes the client to send invalid requests. This is intended to
+             * use for testing SRU servers for protocol conformance (i.e.
+             * provoke an error) and SHOULD NEVER be used in production!
+             */
+            final String malformedVersion =
+                    getExtraRequestData(X_MALFORMED_VERSION);
+            if (malformedVersion == null) {
+                versionPerformed = (version != null) ? version : defaultVersion;
+                switch (versionPerformed) {
+                case VERSION_1_1:
+                    uriBuilder.append(PARAM_VERSION, VERSION_1_1);
+                    break;
+                case VERSION_1_2:
+                    uriBuilder.append(PARAM_VERSION, VERSION_1_2);
+                    break;
+                default:
+                    throw new SRUClientException("unsupported version: " +
+                            versionPerformed);
+                } // switch
+            } else {
+                if (!malformedVersion.equalsIgnoreCase(MALFORMED_OMIT)) {
+                    uriBuilder.append(PARAM_VERSION, malformedVersion);
+                }
+            }
+
+            // request specific parameters
+            addParametersToURI(uriBuilder);
+
+            // extraRequestData
+            if ((extraRequestData != null) && !extraRequestData.isEmpty()) {
+                for (Map.Entry<String, String> entry :
+                    extraRequestData.entrySet()) {
+                    final String key = entry.getKey();
+
+                    /*
+                     * make sure, we skip the client-internal parameters
+                     * to generate invalid requests ...
+                     */
+                    if (!key.startsWith(MALFORMED_KEY_PREFIX)) {
+                        uriBuilder.append(key, entry.getValue());
+                    }
+                }
+            }
+
+            return uriBuilder.makeURI();
+        } catch (URISyntaxException e) {
+            throw new SRUClientException("error while building request URI", e);
+        }
+    }
+
+
+    /**
+     * <em>Note: this method is not a part of public API.</em>
+     * @return a constant for this
+     */
+    abstract SRUOperation getOperation();
+
+
+    abstract void addParametersToURI(URIHelper uriBuilder)
+            throws SRUClientException;
+
+} // class AbstractSRURequest
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUAbstractResponse.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUAbstractResponse.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUAbstractResponse.java	(revision 2998)
@@ -0,0 +1,203 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.w3c.dom.DocumentFragment;
+
+
+
+/**
+ * Abstract base class for SRU responses.
+ *
+ * @see SRUExplainResponse
+ * @see SRUScanResponse
+ * @see SRUSearchRetrieveResponse
+ */
+class SRUAbstractResponse<T extends SRUAbstractRequest> {
+    private final T request;
+    private final List<SRUDiagnostic> diagnostics;
+    private final DocumentFragment extraResponseData;
+    private final int totalBytesTransferred;
+    private final long timeTotal;
+    private final long timeQueued;
+    private final long timeNetwork;
+    private final long timeProcessing;
+
+
+    /**
+     * Constructor.
+     *
+     * @param diagnostics
+     *            a list of diagnostics associated to this result or
+     *            <code>null</code> if none.
+     * @param extraResponseData
+     *            extra response data for this result or <code>null</code> if
+     *            none.
+     * @param totalBytesTransferred
+     *            the total number of bytes transferred for this request
+     * @param timeTotal
+     *            the total number of milliseconds elapsed while performing this
+     *            request
+     * @param timeQueued
+     *            the total number of milliseconds elapsed while this request
+     *            was queued
+     * @param timeNetwork
+     *            the total number of milliseconds elapsed while this request
+     *            waited for network operations to finish
+     * @param timeProcessing
+     *            the total number of milliseconds elapsed while the client
+     *            processed the response from the endpoint
+     */
+    protected SRUAbstractResponse(T request,
+            List<SRUDiagnostic> diagnostics,
+            DocumentFragment extraResponseData,
+            int totalBytesTransferred,
+            long timeTotal,
+            long timeQueued,
+            long timeNetwork,
+            long timeProcessing) {
+        this.request = request;
+        this.diagnostics = ((diagnostics != null) && !diagnostics.isEmpty())
+                ? Collections.unmodifiableList(diagnostics)
+                : null;
+        this.extraResponseData     = extraResponseData;
+        this.totalBytesTransferred = totalBytesTransferred;
+        this.timeTotal             = timeTotal;
+        this.timeQueued            = timeQueued;
+        this.timeNetwork           = timeNetwork;
+        this.timeProcessing        = timeProcessing;
+    }
+
+
+    /**
+     * Get the request that produced this response.
+     *
+     * @return the request
+     */
+    public T getRequest() {
+        return request;
+    }
+
+
+    /**
+     * Get the diagnostics for this response.
+     *
+     * @return diagnostics for this response or <code>null</code> if none
+     */
+    public List<SRUDiagnostic> getDiagnostics() {
+        return diagnostics;
+    }
+
+
+    /**
+     * Check, if the response contains any diagnostics.
+     *
+     * <p>
+     * NB: Surrogate diagnostics are not covered by this.
+     * </p>
+     *
+     * @return <code>true</code> if response contains any diagnostic,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasDiagnostics() {
+        return diagnostics != null;
+    }
+
+
+    /**
+     * Get the extra response data for this result.
+     *
+     * @return get an instance of {@link DocumentFragment} containing the XML
+     *         fragment for the extra response data from the SRU response or
+     *         <code>null</code> if none are available
+     */
+    public DocumentFragment getExtraResponseData() {
+        return extraResponseData;
+    }
+
+
+    /**
+     * Check, if this response has any extra response data attached to it.
+     *
+     * @return <code>true</code> if extra response is attached,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasExtraResponseData() {
+        return extraResponseData != null;
+    }
+
+
+    /**
+     * Get the total number of bytes transferred for this request.
+     *
+     * @return the total number of bytes or <code>-1</code> if not available
+     */
+    public long getTotalBytesTransferred() {
+        return totalBytesTransferred;
+    }
+
+
+    /**
+     * Get the total number of milliseconds elapsed for this request.
+     *
+     * @return the total number of milliseconds or <code>-1</code> if not
+     *         available
+     */
+    public long getTimeTotal() {
+        return timeTotal;
+    }
+
+
+    /**
+     * Get the number of milliseconds this request has been queued before it was
+     * processed by the client.
+     *
+     * @return the number of milliseconds queued or <code>-1</code> if not
+     *         available
+     */
+    public long getTimeWait() {
+        return timeQueued;
+    }
+
+
+    /**
+     * Get the number of milliseconds this request spend waiting for network
+     * operations to finish.
+     *
+     * @return the number of milliseconds spend in waiting on network or
+     *         <code>-1</code> if not available
+     */
+    public long getTimeNetwork() {
+        return timeNetwork;
+    }
+
+
+    /**
+     * Get the number of milliseconds the client was busy processing the results
+     * sent from the endpoint.
+     *
+     * @return the number of milliseconds spend in processing or <code>-1</code>
+     *         if not available
+     */
+    public long getTimeProcessing() {
+        return timeProcessing;
+    }
+
+} // class SRUAbstractResponse
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUCallback.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUCallback.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUCallback.java	(revision 2998)
@@ -0,0 +1,58 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * A interface for creating asynchronous callbacks for use with the
+ * {@link SRUThreadedClient}.
+ *
+ * <p>
+ * NB: the callbacks will be executed by any of worker threads of the client.
+ * </p>
+ *
+ * @param <V>
+ *            the request type
+ * @param <S>
+ *            the response type
+ *
+ * @see SRUThreadedClient#explain(SRUExplainRequest, SRUCallback)
+ * @see SRUThreadedClient#scan(SRUScanRequest, SRUCallback)
+ * @see SRUThreadedClient#searchRetrieve(SRUSearchRetrieveRequest, SRUCallback)
+ */
+public interface SRUCallback<V extends SRUAbstractRequest,
+                             S extends SRUAbstractResponse<V>> {
+
+    /**
+     * Invoked when the request has been completed successfully.
+     *
+     * @param response
+     *            the response to the request
+     */
+    public void onSuccess(S response);
+
+
+    /**
+     * Invoked when the request yielded an error
+     *
+     * @param request
+     *            the original request
+     * @param error
+     *            the error
+     */
+    public void onError(V request, SRUClientException error);
+
+} // interface SRUCallback
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClient.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClient.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClient.java	(revision 2998)
@@ -0,0 +1,513 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+
+/**
+ * A client to perform SRU operations. The response of a SRU request is wrapped
+ * in a SRU response.
+ * <p>
+ * This client is reusable but not thread-safe: the application may reuse a
+ * client object, but it may not be concurrently shared between multiple
+ * threads.
+ * </p>
+ */
+public class SRUClient {
+    private final SRUSimpleClient client;
+    private final Handler handler;
+    /* common */
+    private List<SRUDiagnostic> diagnostics;
+    private DocumentFragment extraResponseData;
+    /* scan */
+    private List<SRUTerm> terms;
+    /* searchRetrieve */
+    private int numberOfRecords;
+    private String resultSetId;
+    private int resultSetIdleTime;
+    private int nextRecordPosition;
+    /* explain/searchRetrieve */
+    private List<SRURecord> records;
+    /* statistics */
+    private int totalBytesTransferred;
+    private long timeTotal;
+    private long timeQueued;
+    private long timeNetwork;
+    private long timeParsing;
+    /* other fields */
+    private final DocumentBuilder documentBuilder;
+    private final Deque<Node> stack = new ArrayDeque<Node>();
+
+
+    /**
+     * Constructor. This constructor will create a <em>strict</em> client and
+     * use the default SRU version.
+     *
+     * @see SRUSimpleClient#DEFAULT_SRU_VERSION
+     */
+    public SRUClient() {
+        this(SRUSimpleClient.DEFAULT_SRU_VERSION,
+                new HashMap<String, SRURecordDataParser>(),
+                DocumentBuilderFactory.newInstance());
+    }
+
+
+    /**
+     * Constructor. This constructor will create a <em>strict</em> client.
+     *
+     * @param defaultVersion
+     *            the default version to use for SRU requests; may be overridden
+     *            by individual requests
+     */
+    public SRUClient(SRUVersion defaultVersion) {
+        this(defaultVersion,
+                new HashMap<String, SRURecordDataParser>(),
+                DocumentBuilderFactory.newInstance());
+    }
+
+
+    /**
+     * Constructor.
+     *
+     * <p>
+     * For internal use only.
+     * </p>
+     *
+     * @param defaultVersion
+     *            the default version to use for SRU requests; may be overridden
+     *            by individual requests
+     * @param parsers
+     *            a <code>Map</code> to store record schema to record data
+     *            parser mappings
+     * @param documentBuilderFactory
+     *            the Document Builder factory to be used to create Document
+     *            Builders
+     */
+    SRUClient(SRUVersion defaultVersion,
+            Map<String, SRURecordDataParser> parsers,
+            DocumentBuilderFactory documentBuilderFactory) {
+        if (defaultVersion == null) {
+            throw new NullPointerException("version == null");
+        }
+        if (parsers == null) {
+            throw new NullPointerException("parsers == null");
+        }
+        if (documentBuilderFactory == null) {
+            throw new NullPointerException("documentBuilderFactory == null");
+        }
+        this.client = new SRUSimpleClient(defaultVersion, parsers);
+        this.handler = new Handler();
+        try {
+            synchronized (documentBuilderFactory) {
+                documentBuilderFactory.setNamespaceAware(true);
+                documentBuilderFactory.setCoalescing(true);
+                this.documentBuilder =
+                        documentBuilderFactory.newDocumentBuilder();
+            } // documentBuilderFactory (documentBuilderFactory)
+        } catch (ParserConfigurationException e) {
+            throw new Error("error initialzing document builder factory", e);
+        }
+        reset();
+    }
+
+
+    /**
+     * Register a record data parser.
+     *
+     * @param parser
+     *            a parser instance
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if the supplied parser is invalid or a parser handing the
+     *             same record schema is already registered
+     */
+    public void registerRecordParser(SRURecordDataParser parser) {
+        client.registerRecordParser(parser);
+    }
+
+
+    /**
+     * Perform a <em>explain</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUExplainRequest} object
+     * @return a {@link SRUExplainResponse} object
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     */
+    public SRUExplainResponse explain(SRUExplainRequest request)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        try {
+            client.explain(request, handler);
+            SRURecord record = null;
+            if ((records != null) && !records.isEmpty()) {
+                record = records.get(0);
+            }
+            return new SRUExplainResponse(request,
+                    diagnostics,
+                    extraResponseData,
+                    totalBytesTransferred,
+                    timeTotal,
+                    timeQueued,
+                    timeNetwork,
+                    timeParsing,
+                    record);
+        } finally {
+            reset();
+        }
+    }
+
+
+    /**
+     * Perform a <em>scan</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUScanRequest} object
+     * @return a {@link SRUScanResponse} object
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     */
+    public SRUScanResponse scan(SRUScanRequest request)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        try {
+            client.scan(request, handler);
+            return new SRUScanResponse(request,
+                    diagnostics,
+                    extraResponseData,
+                    totalBytesTransferred,
+                    timeTotal,
+                    timeQueued,
+                    timeNetwork,
+                    timeParsing,
+                    terms);
+        } finally {
+            reset();
+        }
+
+    }
+
+
+    /**
+     * Perform a <em>searchRetrieve</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUSearchRetrieveRequest} object
+     * @return a {@link SRUSearchRetrieveRequest} object
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     */
+    public SRUSearchRetrieveResponse searchRetrieve(
+            SRUSearchRetrieveRequest request) throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        try {
+            client.searchRetrieve(request, handler);
+            return new SRUSearchRetrieveResponse(request,
+                    diagnostics,
+                    extraResponseData,
+                    totalBytesTransferred,
+                    timeTotal,
+                    timeQueued,
+                    timeNetwork,
+                    timeParsing,
+                    numberOfRecords,
+                    resultSetId,
+                    resultSetIdleTime,
+                    records,
+                    nextRecordPosition);
+        } finally {
+            reset();
+        }
+    }
+
+
+    void setTimeQueued(long timeQueued) {
+        this.timeQueued = TimeUnit.NANOSECONDS.toMillis(timeQueued);
+    }
+
+
+    private void addTerm(SRUTerm term) {
+        if (terms == null) {
+            terms = new LinkedList<SRUTerm>();
+        }
+        terms.add(term);
+    }
+
+
+    private void addRecord(SRURecord record) {
+        if (records == null) {
+            records = new LinkedList<SRURecord>();
+        }
+        records.add(record);
+    }
+
+
+    private void reset() {
+        /* common */
+        diagnostics           = null;
+        extraResponseData     = null;
+        /* scan */
+        terms                 = null;
+        /* searchRetrieve */
+        numberOfRecords       = -1;
+        resultSetId           = null;
+        resultSetIdleTime     = -1;
+        nextRecordPosition    = -1;
+        /* explain/searchRetrieve */
+        records               = null;
+        /* statistics */
+        totalBytesTransferred = -1;
+        timeQueued            = -1;
+        timeTotal             = -1;
+        timeNetwork           = -1;
+        timeParsing           = -1;
+    }
+
+
+    private class Handler extends SRUDefaultHandlerAdapter {
+        @Override
+        public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+                throws SRUClientException {
+            SRUClient.this.diagnostics = diagnostics;
+        }
+
+
+        @Override
+        public void onExtraResponseData(XMLStreamReader reader)
+                throws XMLStreamException, SRUClientException {
+            final List<SRURecord> records = SRUClient.this.records;
+            if ((records != null) && !records.isEmpty()) {
+                final SRURecord record = records.get(records.size() - 1);
+                record.setExtraRecordData(copyStaxToDocumentFragment(
+                        documentBuilder, stack, reader));
+            } else {
+                /*
+                 * should never happen ...
+                 */
+                throw new SRUClientException(
+                        "internal error; 'records' are null or empty");
+            }
+        }
+
+
+        @Override
+        public void onTerm(String value, int numberOfRecords,
+                String displayTerm, SRUWhereInList whereInList)
+                throws SRUClientException {
+            SRUClient.this.addTerm(new SRUTerm(value, numberOfRecords,
+                    displayTerm, whereInList));
+        }
+
+
+        @Override
+        public void onExtraTermData(String value, XMLStreamReader reader)
+                throws XMLStreamException, SRUClientException {
+            final List<SRUTerm> terms = SRUClient.this.terms;
+            if ((terms != null) && !terms.isEmpty()) {
+                SRUTerm term = terms.get(terms.size() - 1);
+                term.setExtraTermData(copyStaxToDocumentFragment(
+                        documentBuilder, stack, reader));
+            } else {
+                /*
+                 * should never happen ...
+                 */
+                throw new SRUClientException(
+                        "internal error; 'terms' is null or empty");
+            }
+        }
+
+
+        @Override
+        public void onStartRecords(int numberOfRecords, String resultSetId,
+                int resultSetIdleTime) throws SRUClientException {
+            SRUClient.this.resultSetId = resultSetId;
+            SRUClient.this.resultSetIdleTime = resultSetIdleTime;
+        }
+
+
+        @Override
+        public void onFinishRecords(int nextRecordPosition)
+                throws SRUClientException {
+            SRUClient.this.nextRecordPosition = nextRecordPosition;
+        }
+
+
+        @Override
+        public void onRecord(String identifier, int position,
+                SRURecordData data) throws SRUClientException {
+            SRUClient.this.addRecord(
+                    new SRURecord(data, identifier, position));
+        }
+
+
+        @Override
+        public void onSurrogateRecord(String identifier, int position,
+                SRUDiagnostic data) throws SRUClientException {
+            SRUClient.this.addRecord(new SRURecord(
+                    new SRUSurrogateRecordData(data), identifier, position));
+        }
+
+
+        @Override
+        public void onExtraRecordData(String identifier, int position,
+                XMLStreamReader reader) throws XMLStreamException,
+                SRUClientException {
+            extraResponseData =
+                    copyStaxToDocumentFragment(documentBuilder, stack, reader);
+        }
+
+
+        @Override
+        public void onRequestStatistics(int totalBytesTransferred,
+                long millisTotal, long millisNetwork, long millisProcessing) {
+            SRUClient.this.totalBytesTransferred = totalBytesTransferred;
+            if (SRUClient.this.timeQueued > 0) {
+                SRUClient.this.timeTotal = timeQueued  + millisTotal;
+            } else {
+                SRUClient.this.timeTotal = millisTotal;
+            }
+            SRUClient.this.timeNetwork = millisNetwork;
+            SRUClient.this.timeParsing = millisProcessing;
+        }
+
+    } // inner class Handler
+
+
+    private static DocumentFragment copyStaxToDocumentFragment(
+            DocumentBuilder builder, Deque<Node> stack, XMLStreamReader reader)
+            throws XMLStreamException {
+        try {
+            final Document doc = builder.newDocument();
+            stack.push(doc.createDocumentFragment());
+
+            while (reader.hasNext()) {
+                final Node parent = stack.peek();
+                switch (reader.getEventType()) {
+                case XMLStreamConstants.START_ELEMENT:
+                    stack.push(createElementNode(parent, doc, reader));
+                    break;
+                case XMLStreamConstants.END_ELEMENT:
+                    stack.pop();
+                    break;
+                case XMLStreamConstants.CHARACTERS:
+                    parent.appendChild(doc.createTextNode(reader.getText()));
+                    break;
+                case XMLStreamConstants.COMMENT:
+                    parent.appendChild(doc.createComment(reader.getText()));
+                    break;
+                case XMLStreamConstants.CDATA:
+                    parent.appendChild(doc.createCDATASection(reader.getText()));
+                    break;
+                default:
+                    break;
+                }
+                reader.next();
+            } // while
+            if (stack.size() != 1) {
+                throw new XMLStreamException(
+                        "internal error; stack should hold only one element");
+            }
+            return (DocumentFragment) stack.pop();
+        } catch (DOMException e) {
+            throw new XMLStreamException(
+                    "error creating document fragment", e);
+        }
+    }
+
+
+    private static Element createElementNode(Node parent, Document doc,
+            XMLStreamReader reader) throws XMLStreamException, DOMException {
+        Element element = doc.createElementNS(reader.getNamespaceURI(),
+                reader.getLocalName());
+
+        if ((reader.getPrefix() != null) && !reader.getPrefix().isEmpty()) {
+            element.setPrefix(reader.getPrefix());
+        }
+
+        parent.appendChild(element);
+
+        // add namespace declarations
+        for (int i = 0; i < reader.getNamespaceCount(); i++) {
+            final String uri    = reader.getNamespaceURI(i);
+            final String prefix = reader.getNamespacePrefix(i);
+
+            if ((prefix != null) && !prefix.isEmpty()) {
+                element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI,
+                        XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix,
+                        uri);
+            } else {
+                if (uri != null) {
+                    element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI,
+                            XMLConstants.XMLNS_ATTRIBUTE,
+                            uri);
+                }
+            }
+        }
+
+        // add other attributes
+        for (int i = 0; i < reader.getAttributeCount(); i++) {
+            String name   = reader.getAttributeLocalName(i);
+            String prefix = reader.getAttributePrefix(i);
+            if (prefix != null && prefix.length() > 0) {
+                name = prefix + ":" + name;
+            }
+
+            Attr attr = doc.createAttributeNS(
+                    reader.getAttributeNamespace(i), name);
+            attr.setValue(reader.getAttributeValue(i));
+            element.setAttributeNode(attr);
+        }
+        return element;
+    }
+
+} // class SRUClient
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientConstants.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientConstants.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientConstants.java	(revision 2998)
@@ -0,0 +1,27 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * Some constants for the SRUClient.
+ */
+public class SRUClientConstants {
+
+    /** constant record data schema parser to match any schema */
+    public static final String RECORD_DATA_PARSER_SCHEMA_ANY = "*";
+
+} // class SRUClientConstants
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientDiagnostics.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientDiagnostics.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientDiagnostics.java	(revision 2998)
@@ -0,0 +1,42 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * Diagnostics defined by SRU client.
+ */
+public final class SRUClientDiagnostics {
+    private static final String URI_PREFIX = "info:clarin/sru/diagnostic/";
+    /**
+     * The record parser erroneously returned a null pointer instead of a record
+     * data object.
+     */
+    public static final String DIAG_RECORD_PARSER_NULL = URI_PREFIX + "1";
+    /**
+     * No record parser was found for this record schema. The name of the record
+     * schema is supplied in the diagnostic's details.
+     */
+    public static final String DIAG_NO_RECORD_PARSER = URI_PREFIX + "2";
+
+
+    /*
+     * hide constructor
+     */
+    private SRUClientDiagnostics() {
+    }
+
+} // class SRUClientDiagnostics
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientException.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientException.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUClientException.java	(revision 2998)
@@ -0,0 +1,45 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * SRU client exception. 
+ */
+@SuppressWarnings("serial")
+public class SRUClientException extends Exception {
+
+    /**
+     * Constructor
+     * 
+     * @param message an error message
+     */
+    public SRUClientException(String message) {
+        super(message);
+    }
+
+
+    /**
+     * Constructor
+     *  
+     * @param message an error message
+     * @param cause the cause of the error
+     */
+    public SRUClientException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+} // class SRUClientException
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDefaultHandler.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDefaultHandler.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDefaultHandler.java	(revision 2998)
@@ -0,0 +1,40 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * A handler implementing all interfaces for all three response handler types.
+ * 
+ * <p>
+ * This class is available as a convenience base class for SRU applications: it
+ * implements the three response handler interfaces:
+ * </p>
+ * <ul>
+ * <li>{@link SRUExplainHandler}</li>
+ * <li>{@link SRUScanHandler}</li>
+ * <li>{@link SRUSearchRetrieveHandler}</li>
+ * </ul>
+ * 
+ * @see SRUExplainHandler
+ * @see SRUScanHandler
+ * @see SRUSearchRetrieveHandler
+ */
+public interface SRUDefaultHandler extends SRUExplainHandler, SRUScanHandler,
+        SRUSearchRetrieveHandler {
+
+} // interface SRUDefaultHandler
+
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDefaultHandlerAdapter.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDefaultHandlerAdapter.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDefaultHandlerAdapter.java	(revision 2998)
@@ -0,0 +1,118 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+
+/**
+ * Default base class for SRU client response handlers.
+ * <p>
+ * This class is available as a convenience base class for SRU client
+ * applications: it provides default implementations for all of the callbacks in
+ * the three request handler handler classes:
+ * </p>
+ * <ul>
+ *  <li>{@link SRUExplainHandler}</li>
+ *  <li>{@link SRUScanHandler}</li>
+ *  <li>{@link SRUSearchRetrieveHandler}</li>
+ * </ul>
+ * <p>
+ * Application writers can extend this class when they need to implement only
+ * part of an interface; parser writers can instantiate this class to provide
+ * default handlers when the application has not supplied its own.
+ * </p>
+ * 
+ * @see SRUExplainHandler
+ * @see SRUScanHandler
+ * @see SRUSearchRetrieveHandler
+ */
+public class SRUDefaultHandlerAdapter implements SRUDefaultHandler {
+    
+    @Override
+    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+            throws SRUClientException {
+    }
+
+    @Override
+    public void onRequestStatistics(int bytes, long millisTotal,
+            long millisNetwork, long millisParsing) {
+    }
+
+
+    @Override
+    public void onExtraResponseData(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+    }
+
+
+    @Override
+    public void onStartTerms() throws SRUClientException {
+    }
+
+
+    @Override
+    public void onFinishTerms() throws SRUClientException {
+    }
+
+
+    @Override
+    public void onTerm(String value, int numberOfRecords, String displayTerm,
+            SRUWhereInList whereInList) throws SRUClientException {
+    }
+
+
+    @Override
+    public void onExtraTermData(String value, XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+    }
+
+
+    @Override
+    public void onStartRecords(int numberOfRecords, String resultSetId,
+            int resultSetIdleTime) throws SRUClientException {
+    }
+
+
+    @Override
+    public void onFinishRecords(int nextRecordPosition)
+            throws SRUClientException {
+    }
+
+
+    @Override
+    public void onRecord(String identifier, int position, SRURecordData data)
+            throws SRUClientException {
+    }
+
+
+    @Override
+    public void onSurrogateRecord(String identifier, int position,
+            SRUDiagnostic data) throws SRUClientException {
+    }
+
+
+    @Override
+    public void onExtraRecordData(String identifier, int position,
+            XMLStreamReader reader) throws XMLStreamException,
+            SRUClientException {
+    }
+
+} // class SRUDefaultHandler
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDiagnostic.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDiagnostic.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUDiagnostic.java	(revision 2998)
@@ -0,0 +1,83 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * Class to hold a SRU diagnostic.
+ * 
+ * @see <a href="http://www.loc.gov/standards/sru/specs/diagnostics.html">SRU
+ *      Diagnostics</a>
+ * @see <a
+ *      href="http://www.loc.gov/standards/sru/resources/diagnostics-list.html">SRU
+ *      Diagnostics List</a>
+ */
+public final class SRUDiagnostic {
+    private final String uri;
+    private final String details;
+    private final String message;
+
+
+    /**
+     * Constructor.
+     * 
+     * @param uri
+     *            the URI identifying the diagnostic
+     * @param details
+     *            supplementary information available, often in a format
+     *            specified by the diagnostic or <code>null</code>
+     * @param message
+     *            human readable message to display to the end user or
+     *            <code>null</code>
+     */
+    public SRUDiagnostic(String uri, String details, String message) {
+        this.uri = uri;
+        this.details = details;
+        this.message = message;
+    }
+
+
+    /**
+     * Get diagnostic's identifying URI.
+     * 
+     * @return diagnostic code
+     */
+    public String getURI() {
+        return uri;
+    }
+
+
+    /**
+     * Get supplementary information for this diagnostic. The format for this
+     * value is often specified by the diagnostic code.
+     * 
+     * @return supplementary information
+     */
+    public String getDetails() {
+        return details;
+    }
+
+
+    /**
+     * Get human readable message.
+     * 
+     * @return human readable message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+} // class SRUDiagnostic
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainHandler.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainHandler.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainHandler.java	(revision 2998)
@@ -0,0 +1,146 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+
+/**
+ * Receive notifications to the response of a <em>explain</em> request.
+ *
+ * @see SRUExplainRequest
+ * @see <a href="http://www.loc.gov/standards/sru/specs/explain.html">SRU Explain Operation</a>
+ */
+public interface SRUExplainHandler {
+
+    /**
+     * Receive notification of diagnostics.
+     *
+     * @param diagnostics
+     *            a list of {@link SRUDiagnostic}
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRUDiagnostic
+     */
+    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of request statistics.
+     *
+     * @param totalBytesTransferred
+     *            the total number of bytes transferred while receiving the
+     *            response
+     * @param millisTotal
+     *            the total time spend processing the request
+     * @param millisNetwork
+     *            the time spend performing network operations
+     * @param millisProcessing
+     *            the time spend processing the response
+     */
+    public void onRequestStatistics(int totalBytesTransferred,
+            long millisTotal, long millisNetwork, long millisProcessing);
+
+
+    /**
+     * Receive notification of extra response data.
+     *
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the extra response data
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see <a href="http://www.loc.gov/standards/sru/specs/extra-data.html">SRU
+     *      Extra Data / Extensions</a>
+     */
+    public void onExtraResponseData(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException;
+
+    /**
+     * Receive notifications of the start of the enumeration of records in the
+     * response.
+     *
+     * @param numberOfRecords
+     *            the number of records or <code>-1</code> if not available
+     * @param resultSetId
+     *            the result set id or <code>null</code> if not available
+     * @param resultSetIdleTime
+     *            the result set idle time or <code>-1</code> if not available
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onStartRecords(int numberOfRecords, String resultSetId,
+            int resultSetIdleTime) throws SRUClientException;
+
+
+    /**
+     * Receive notifications of the end of the enumeration of records in the
+     * response.
+     *
+     * @param nextRecordPosition
+     *            the next record position or <code>-1</code> if not available
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onFinishRecords(int nextRecordPosition)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of a record in the result set.
+     *
+     * @param identifier
+     *            identifier of the record or <code>null</code> if not available
+     * @param position
+     *            position of the record in the result set or <code>-1</code> if
+     *            not available
+     * @param data
+     *            the parsed record data
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRURecordData
+     * @see SRURecordDataParser
+     */
+    public void onRecord(String identifier, int position, SRURecordData data)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of extra record data.
+     *
+     * @param identifier
+     *            identifier of the record or <code>null</code> if not available
+     * @param position
+     *            position of the record in the result set or <code>-1</code> if
+     *            not available
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the extra term data
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onExtraRecordData(String identifier, int position,
+            XMLStreamReader reader) throws XMLStreamException,
+            SRUClientException;
+
+} // interface SRUExplainHandler
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRecordData.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRecordData.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRecordData.java	(revision 2998)
@@ -0,0 +1,558 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A record data implementation for SRU explain record data conforming to the
+ * ZeeRex schema.
+ *
+ * @see <a href="http://zeerex.z3950.org/dtd/">ZeeRex DTD</a>
+ */
+public class SRUExplainRecordData implements SRURecordData {
+    public static final String RECORD_SCHEMA =
+            "http://explain.z3950.org/dtd/2.0/";
+
+    public static class ServerInfo {
+        private final String host;
+        private final int port;
+        private final String database;
+        private final SRUVersion version;
+        private final Set<String> transport;
+
+        ServerInfo(String host, int port, String database,
+                String protocol, SRUVersion version, Set<String> transport) {
+            if (host == null) {
+                throw new NullPointerException("host == null");
+            }
+            if (host.isEmpty()) {
+                throw new IllegalArgumentException("host is empty");
+            }
+            if ((port < 0) || (port > 65535)) {
+                throw new IllegalArgumentException("port < 0 || port > 65535");
+            }
+            if (database == null) {
+                throw new NullPointerException("database == null");
+            }
+            if (database.isEmpty()) {
+                throw new IllegalArgumentException("database is empty");
+            }
+            if (version == null) {
+                throw new NullPointerException("version == null");
+            }
+            if (transport == null) {
+                throw new NullPointerException("transport == null");
+            }
+            this.host      = host;
+            this.port      = port;
+            this.database  = database;
+            this.version   = version;
+            this.transport = Collections.unmodifiableSet(transport);
+        }
+
+
+        public String getHost() {
+            return host;
+        }
+
+
+        public int getPort() {
+            return port;
+        }
+
+
+        public String getDatabase() {
+            return database;
+        }
+
+
+        public SRUVersion getVersion() {
+            return version;
+        }
+
+
+        public Set<String> getTransport() {
+            return transport;
+        }
+    } // class ServerInfo
+
+    public static final class LocalizedString {
+        private final boolean primary;
+        private final String lang;
+        private final String value;
+
+
+        LocalizedString(String value, String lang, boolean primary) {
+            this.value = value;
+            this.lang = lang;
+            this.primary = primary;
+        }
+
+
+        public boolean isPrimary() {
+            return primary;
+        }
+
+
+        public String getLang() {
+            return lang;
+        }
+
+
+        public String getValue() {
+            return value;
+        }
+    } // class LocalizedString
+
+    public static final class DatabaseInfo {
+        private final List<LocalizedString> title;
+        private final List<LocalizedString> description;
+        private final List<LocalizedString> author;
+        private final List<LocalizedString> contact;
+        private final List<LocalizedString> extent;
+        private final List<LocalizedString> history;
+        private final List<LocalizedString> langUsage;
+        private final List<LocalizedString> restrictions;
+        private final List<LocalizedString> subjects;
+        private final List<LocalizedString> links;
+        private final List<LocalizedString> implementation;
+
+
+        DatabaseInfo(List<LocalizedString> title,
+                List<LocalizedString> description,
+                List<LocalizedString> author, List<LocalizedString> contact,
+                List<LocalizedString> extent, List<LocalizedString> history,
+                List<LocalizedString> langUsage,
+                List<LocalizedString> restrictions,
+                List<LocalizedString> subjects, List<LocalizedString> links,
+                List<LocalizedString> implementation) {
+            if ((title != null) && !title.isEmpty()) {
+                this.title = Collections.unmodifiableList(title);
+            } else {
+                this.title = null;
+            }
+            if ((description != null) && !description.isEmpty()) {
+                this.description = Collections.unmodifiableList(description);
+            } else {
+                this.description = null;
+            }
+            if ((author != null) && !author.isEmpty()) {
+                this.author = Collections.unmodifiableList(author);
+            } else {
+                this.author = null;
+            }
+            if ((contact != null) && !contact.isEmpty()) {
+                this.contact = Collections.unmodifiableList(contact);
+            } else {
+                this.contact = null;
+            }
+            if ((extent != null) && !extent.isEmpty()) {
+                this.extent = Collections.unmodifiableList(extent);
+            } else {
+                this.extent = null;
+            }
+            if ((history != null) && !history.isEmpty()) {
+                this.history = Collections.unmodifiableList(history);
+            } else {
+                this.history = null;
+            }
+            if ((langUsage != null) && !langUsage.isEmpty()) {
+                this.langUsage = Collections.unmodifiableList(langUsage);
+            } else {
+                this.langUsage = null;
+            }
+            if ((restrictions != null) && !restrictions.isEmpty()) {
+                this.restrictions = Collections.unmodifiableList(restrictions);
+            } else {
+                this.restrictions = null;
+            }
+            if ((subjects != null) && !subjects.isEmpty()) {
+                this.subjects = Collections.unmodifiableList(subjects);
+            } else {
+                this.subjects = null;
+            }
+            if ((links != null) && !links.isEmpty()) {
+                this.links = Collections.unmodifiableList(links);
+            } else {
+                this.links = null;
+            }
+            if ((implementation != null) && !implementation.isEmpty()) {
+                this.implementation = Collections
+                        .unmodifiableList(implementation);
+            } else {
+                this.implementation = null;
+            }
+        }
+
+
+        public List<LocalizedString> getTitle() {
+            return title;
+        }
+
+
+        public List<LocalizedString> getDescription() {
+            return description;
+        }
+
+
+        public List<LocalizedString> getAuthor() {
+            return author;
+        }
+
+
+        public List<LocalizedString> getContact() {
+            return contact;
+        }
+
+
+        public List<LocalizedString> getExtend() {
+            return extent;
+        }
+
+
+        public List<LocalizedString> getHistory() {
+            return history;
+        }
+
+
+        public List<LocalizedString> getLangUsage() {
+            return langUsage;
+        }
+
+
+        public List<LocalizedString> getRestrictions() {
+            return restrictions;
+        }
+
+
+        public List<LocalizedString> getSubjects() {
+            return subjects;
+        }
+
+
+        public List<LocalizedString> getLinks() {
+            return links;
+        }
+
+
+        public List<LocalizedString> getImplementation() {
+            return implementation;
+        }
+    } // class DatabaseInfo
+
+    public static final class IndexInfo {
+        public static class Set {
+            private final String identifier;
+            private final String name;
+            private final List<LocalizedString> title;
+
+
+            Set(String identifier, String name, List<LocalizedString> title) {
+                this.identifier = identifier;
+                this.name = name;
+                if ((title != null) && !title.isEmpty()) {
+                    this.title = Collections.unmodifiableList(title);
+                } else {
+                    this.title = null;
+                }
+            }
+
+
+            public String getIdentifier() {
+                return identifier;
+            }
+
+
+            public String getName() {
+                return name;
+            }
+
+
+            public List<LocalizedString> getTitle() {
+                return title;
+            }
+        } // class IndexInfo.Set
+
+        public static class Index {
+            public static class Map {
+                private final boolean primary;
+                private final String set;
+                private final String name;
+
+
+                Map(boolean primary, String set, String name) {
+                    this.primary = primary;
+                    this.set     = set;
+                    this.name    = name;
+                }
+
+
+                public boolean isPrimary() {
+                    return primary;
+                }
+
+
+                public String getSet() {
+                    return set;
+                }
+
+
+                public String getName() {
+                    return name;
+                }
+            } // class IndexInfo.Index.Map
+
+            private final String id;
+            private final List<LocalizedString> title;
+            private final boolean can_search;
+            private final boolean can_scan;
+            private final boolean can_sort;
+            private final List<Index.Map> maps;
+
+
+            Index(String id, List<LocalizedString> title, boolean can_search,
+                    boolean can_scan, boolean can_sort, List<Map> maps) {
+                this.id = id;
+                if ((title != null) && !title.isEmpty()) {
+                    this.title = Collections.unmodifiableList(title);
+                } else {
+                    this.title = null;
+                }
+                this.can_search = can_search;
+                this.can_scan = can_scan;
+                this.can_sort = can_sort;
+                this.maps = maps;
+            }
+
+
+            public String getId() {
+                return id;
+            }
+
+
+            public List<LocalizedString> getTitle() {
+                return title;
+            }
+
+
+            public boolean canSearch() {
+                return can_search;
+            }
+
+
+            public boolean canScan() {
+                return can_scan;
+            }
+
+
+            public boolean canSort() {
+                return can_sort;
+            }
+
+
+            public List<Index.Map> getMaps() {
+                return maps;
+            }
+
+        } // class Index
+
+        private final List<IndexInfo.Set> sets;
+        private final List<IndexInfo.Index> indexes;
+
+
+        IndexInfo(List<IndexInfo.Set> sets, List<IndexInfo.Index> indexes) {
+            if ((sets != null) && !sets.isEmpty()) {
+                this.sets = Collections.unmodifiableList(sets);
+            } else {
+                this.sets = null;
+            }
+            if ((indexes != null) && !indexes.isEmpty()) {
+                this.indexes = Collections.unmodifiableList(indexes);
+            } else {
+                this.indexes = null;
+            }
+        }
+
+
+        public List<IndexInfo.Set> getSets() {
+            return sets;
+        }
+
+
+        public List<IndexInfo.Index> getIndexes() {
+            return indexes;
+        }
+    } // class IndexInfo
+
+    public static class Schema {
+        private final String identifier;
+        private final String name;
+        private final String location;
+        private final boolean sort;
+        private final boolean retrieve;
+        private final List<LocalizedString> title;
+
+
+        Schema(String identifier, String name, String location, boolean sort,
+                boolean retrieve, List<LocalizedString> title) {
+            this.identifier = identifier;
+            this.name       = name;
+            this.location   = location;
+            this.sort       = sort;
+            this.retrieve   = retrieve;
+            if ((title != null) && !title.isEmpty()) {
+                this.title = Collections.unmodifiableList(title);
+            } else {
+                this.title = null;
+            }
+        }
+
+
+        public String getIdentifier() {
+            return identifier;
+        }
+
+
+        public String getName() {
+            return name;
+        }
+
+
+        public String getLocation() {
+            return location;
+        }
+
+
+        public boolean getSort() {
+            return sort;
+        }
+
+
+        public boolean getRetrieve() {
+            return retrieve;
+        }
+
+
+        public List<LocalizedString> getTitle() {
+            return title;
+        }
+    } // class SchemaInfo
+
+    public static class ConfigInfo {
+        private final Map<String, String> defaults;
+        private final Map<String, String> settings;
+        private final Map<String, String> supports;
+
+
+        ConfigInfo(Map<String, String> defaults, Map<String, String> settings,
+                Map<String, String> supports) {
+            if ((defaults != null) && !defaults.isEmpty()) {
+                this.defaults = Collections.unmodifiableMap(defaults);
+            } else {
+                this.defaults = Collections.emptyMap();
+            }
+            if ((settings != null) && !settings.isEmpty()) {
+                this.settings = Collections.unmodifiableMap(settings);
+            } else {
+                this.settings = Collections.emptyMap();
+            }
+            if ((supports != null) && !supports.isEmpty()) {
+                this.supports = Collections.unmodifiableMap(supports);
+            } else {
+                this.supports = Collections.emptyMap();
+            }
+        }
+
+
+        public Map<String, String> getDefaults() {
+            return defaults;
+        }
+
+
+        public Map<String, String> getSettings() {
+            return settings;
+        }
+
+
+        public Map<String, String> getSupports() {
+            return supports;
+        }
+    } // class ConfigInfo
+
+    private final ServerInfo serverInfo;
+    private final DatabaseInfo databaseInfo;
+    private final IndexInfo indexInfo;
+    private final List<Schema> schemaInfo;
+    private final ConfigInfo configInfo;
+
+
+    SRUExplainRecordData(ServerInfo serverInfo, DatabaseInfo databaseInfo,
+            IndexInfo indexInfo, List<Schema> schemaInfo,
+            ConfigInfo configInfo) {
+        this.serverInfo   = serverInfo;
+        this.databaseInfo = databaseInfo;
+        this.indexInfo    = indexInfo;
+        if ((schemaInfo != null) && !schemaInfo.isEmpty()) {
+            this.schemaInfo = Collections.unmodifiableList(schemaInfo);
+        } else {
+            this.schemaInfo = null;
+        }
+        this.configInfo   = configInfo;
+    }
+
+
+    @Override
+    public boolean isTransient() {
+        return false;
+    }
+
+
+    @Override
+    public String getRecordSchema() {
+        return RECORD_SCHEMA;
+    }
+
+
+    public ServerInfo getServerInfo() {
+        return serverInfo;
+    }
+
+
+    public DatabaseInfo getDatabaseInfo() {
+        return databaseInfo;
+    }
+
+
+    public List<Schema> getSchemaInfo() {
+        return schemaInfo;
+    }
+
+
+    public IndexInfo getIndexInfo() {
+        return indexInfo;
+    }
+
+
+    public ConfigInfo getConfigInfo() {
+        return configInfo;
+    }
+
+} // class SRUExplainRecordData
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRecordDataParser.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRecordDataParser.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRecordDataParser.java	(revision 2998)
@@ -0,0 +1,722 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUExplainRecordData.ConfigInfo;
+import eu.clarin.sru.client.SRUExplainRecordData.DatabaseInfo;
+import eu.clarin.sru.client.SRUExplainRecordData.IndexInfo;
+import eu.clarin.sru.client.SRUExplainRecordData.LocalizedString;
+import eu.clarin.sru.client.SRUExplainRecordData.Schema;
+import eu.clarin.sru.client.SRUExplainRecordData.ServerInfo;
+
+
+/**
+ * This class implements a (partial) record data parser for SRU explain record
+ * data conforming to the ZeeRex schema.
+ *
+ * @see <a href="http://zeerex.z3950.org/dtd/">ZeeRex DTD</a>
+ */
+class SRUExplainRecordDataParser {
+    private static final String ZEEREX_NS       =
+            SRUExplainRecordData.RECORD_SCHEMA;
+    private static final String ZEEREX_NS_QUIRK =
+            "http://explain.z3950.org/dtd/2.1/";
+    private static final String VERSION_1_1     = "1.1";
+    private static final String VERSION_1_2     = "1.2";
+    private static final String TRANSPORT_HTTP  = "http";
+    private static final String TRANSPORT_HTTPS = "https";
+    private static final String STRING_TRUE     = "true";
+    private static final String PRIMARY_FALSE   = "false";
+    private static final Logger logger =
+            LoggerFactory.getLogger(SRUExplainRecordDataParser.class);
+
+
+    public SRURecordData parse(XMLStreamReader reader, SRUVersion version,
+            boolean strict, String recordSchema) throws XMLStreamException,
+            SRUClientException {
+        // sanity check, if we are dealing with the expected record schema
+        if (!(ZEEREX_NS.equals(recordSchema) ||
+                ZEEREX_NS_QUIRK.equals(recordSchema))) {
+            throw new SRUClientException("record schema '" + recordSchema +
+                    "' not supported in explain response record data");
+        }
+
+        logger.debug("parsing explain record data (version={}, schema={})",
+                version, recordSchema);
+
+        // explain
+        if (XmlStreamReaderUtils.peekStart(reader, ZEEREX_NS, "explain")) {
+            return parseExplain(reader, version, strict, ZEEREX_NS);
+        } else if (XmlStreamReaderUtils.peekStart(reader,
+                ZEEREX_NS_QUIRK, "explain")) {
+            if (strict) {
+                logger.warn("namespace '{}' is not defined by ZeeRex, " +
+                        "enabling quirks mode (consider using namespace '{}'" +
+                        " which is defined)", ZEEREX_NS_QUIRK, ZEEREX_NS);
+            } else {
+                logger.info("namespace '{}' is not defined by ZeeRex, " +
+                        "enabling quirks mode (consider using namespace '{}'" +
+                        " which is defined)", ZEEREX_NS_QUIRK, ZEEREX_NS);
+            }
+            return parseExplain(reader, version, strict, ZEEREX_NS_QUIRK);
+        } else {
+            throw new XMLStreamException("expected start tag for element '" +
+                    new QName(ZEEREX_NS, "explain") + " at this position",
+                    reader.getLocation());
+        }
+    }
+
+
+    private static SRURecordData parseExplain(XMLStreamReader reader,
+            SRUVersion version, boolean strict, String ns)
+            throws XMLStreamException, SRUClientException {
+
+        // explain
+        XmlStreamReaderUtils.readStart(reader, ns, "explain", true);
+
+        /*
+         * explain (serverInfo, databaseInfo?, metaInfo?, indexInfo?,
+         *     (recordInfo|schemaInfo)?, configInfo?)
+         */
+
+        // explain/serverInfo (mandatory)
+        ServerInfo serverInfo = parseServerInfo(reader, ns);
+
+        // explain/databaseInfo (optional)
+        DatabaseInfo databaseInfo = null;
+        if (XmlStreamReaderUtils.readStart(reader, ns, "databaseInfo", false)) {
+            databaseInfo = parseDatabaseInfo(reader, ns, strict);
+            XmlStreamReaderUtils.readEnd(reader, ns, "databaseInfo");
+        }
+
+        // explain/metaInfo (optional)
+        if (XmlStreamReaderUtils.readStart(reader, ns, "metaInfo", false)) {
+            /*
+             * metaInfo (dateModified, (aggregatedFrom, dateAggregated)?)
+             */
+            logger.debug("metaInfo");
+            XmlStreamReaderUtils.readEnd(reader, ns, "metaInfo", true);
+        }
+
+        // explain/indexInfo (optional)
+        IndexInfo indexInfo = null;
+        if (XmlStreamReaderUtils.readStart(reader, ns, "indexInfo", false)) {
+            indexInfo = parseIndexInfo(reader, strict, ns);
+            XmlStreamReaderUtils.readEnd(reader, ns, "indexInfo");
+        } // if
+
+        // explain/recordInfo or explain/schemaInfo
+        List<Schema> schemaInfo = null;
+        if (XmlStreamReaderUtils.peekStart(reader, ns, "recordInfo") ||
+            XmlStreamReaderUtils.peekStart(reader, ns, "schemaInfo")) {
+            if (XmlStreamReaderUtils.readStart(reader,
+                    ns, "recordInfo", false)) {
+                logger.debug("skipping 'recordInfo'");
+                XmlStreamReaderUtils.readEnd(reader, ns, "recordInfo", true);
+            } else if (XmlStreamReaderUtils.readStart(reader,
+                    ns, "schemaInfo", false)) {
+                schemaInfo = parseSchemaInfo(reader, strict, ns);
+                XmlStreamReaderUtils.readEnd(reader, ns, "schemaInfo");
+            } else {
+                throw new XMLStreamException("unexpected start element '" +
+                        reader.getName() + "'", reader.getLocation());
+            }
+        }
+
+        // explain/configInfo (optional)
+        ConfigInfo configInfo = null;
+        if (XmlStreamReaderUtils.readStart(reader, ns, "configInfo", false)) {
+            configInfo = parseConfigInfo(reader, strict, ns);
+            XmlStreamReaderUtils.readEnd(reader, ns, "configInfo");
+        }
+
+        XmlStreamReaderUtils.readEnd(reader, ns, "explain", true);
+        return new SRUExplainRecordData(serverInfo, databaseInfo, indexInfo,
+                schemaInfo, configInfo);
+    }
+
+
+    private static ServerInfo parseServerInfo(XMLStreamReader reader,
+            String ns) throws XMLStreamException, SRUClientException {
+        XmlStreamReaderUtils.readStart(reader, ns,
+                "serverInfo", true, true);
+        String protocol = XmlStreamReaderUtils.readAttributeValue(reader,
+                null, "protocol");
+
+        SRUVersion version = SRUVersion.VERSION_1_2;
+        String s = XmlStreamReaderUtils.readAttributeValue(reader, null,
+                "version");
+        if (s != null) {
+            if (VERSION_1_1.equals(s)) {
+                version = SRUVersion.VERSION_1_1;
+            } else if (VERSION_1_2.equals(s)) {
+                version = SRUVersion.VERSION_1_2;
+            } else {
+                throw new SRUClientException("invalid or unsupported value '" +
+                        s + "'for attribute 'version' on element '" +
+                        reader.getName() + "'");
+            }
+        }
+
+        Set<String> transports = new HashSet<String>();
+        s = XmlStreamReaderUtils.readAttributeValue(reader, null, "transport");
+        if (s != null) {
+            for (String i : s.split("\\s+")) {
+                String t = null;
+                if (TRANSPORT_HTTP.equalsIgnoreCase(i)) {
+                    t = TRANSPORT_HTTP;
+                } else if (TRANSPORT_HTTPS.equalsIgnoreCase(i)) {
+                    t = TRANSPORT_HTTPS;
+                } else {
+                    throw new SRUClientException("invalid value '" + i +
+                            "' for attribute 'transport' on element '" +
+                            reader.getName() +
+                            " (use either 'http' of 'https' or both " +
+                            "seperated by whitespace");
+                }
+                if (t != null) {
+                    if (!transports.contains(t)) {
+                        transports.add(t);
+                    } else {
+                        logger.warn("value '{}' already listed in " +
+                                "'transport' attribute of element '{}'",
+                                t, reader.getName());
+                    }
+                }
+
+            } // for
+        } else {
+            transports.add(TRANSPORT_HTTP);
+        }
+        XmlStreamReaderUtils.consumeStart(reader);
+
+        final String host = XmlStreamReaderUtils.readContent(reader,
+                ns, "host", true);
+        final int port = XmlStreamReaderUtils.readContent(reader,
+                ns, "port", true, -1);
+        if ((port < 0) || (port > 65535)) {
+            // FIXME: error message
+            throw new SRUClientException("invalid port number (" + port + ")");
+        }
+        String database = XmlStreamReaderUtils.readContent(reader, ns,
+                "database", true);
+        if (XmlStreamReaderUtils.readStart(reader, ns,
+                "authentication", false)) {
+            XmlStreamReaderUtils.readEnd(reader, ns,
+                    "authentication", true);
+        }
+        XmlStreamReaderUtils.readEnd(reader, ns, "serverInfo", true);
+        logger.debug("serverInfo: host={}, port={}, database={}, version={}, " +
+                "protocol={}, transport={}", host, port, database, version,
+                protocol, transports);
+        return new ServerInfo(host, port, database, protocol, version,
+                transports);
+    }
+
+
+    private static DatabaseInfo parseDatabaseInfo(XMLStreamReader reader,
+            String ns, boolean strict) throws XMLStreamException,
+            SRUClientException {
+        /*
+         * databaseInfo (title*, description*, (author | contact | extent |
+         *     history | langUsage | restrictions | subjects | links |
+         *     implementation)*)
+         */
+
+        List<LocalizedString> title = null;
+        List<LocalizedString> description = null;
+        List<LocalizedString> author = null;
+        List<LocalizedString> contact = null;
+        List<LocalizedString> extent = null;
+        List<LocalizedString> history = null;
+        List<LocalizedString> langUsage = null;
+        List<LocalizedString> restrictions = null;
+        List<LocalizedString> subjects = null;
+        List<LocalizedString> links = null;
+        List<LocalizedString> implementation = null;
+
+        byte mode = 0; /* 1 = title, 2 = description, 3 = others */
+        for (;;) {
+            if (XmlStreamReaderUtils.peekStart(reader, ns, "title")) {
+                if ((mode != 0) && (mode != 1)) {
+                    if (strict) {
+                        throw new XMLStreamException(
+                                "element '" + new QName(ns, "title") +
+                                        "' not allowed here [required content" +
+                                        " model is: title*, description*, " +
+                                        "(author | contact | extent | history" +
+                                        " | langUsage | restrictions | " +
+                                        "subjects | links | implementation)*]",
+                                        reader.getLocation());
+                    } else {
+                        logger.warn("element '{}' not allowed here [required " +
+                                "content model is: title*, description*, " +
+                                "(author | contact | extent | history | " +
+                                "langUsage | restrictions | subjects | links " +
+                                "| implementation)*] at position " +
+                                "[row, col]: [{}, {}]",
+                                new QName(ns, "title"),
+                                reader.getLocation().getLineNumber(),
+                                reader.getLocation().getColumnNumber());
+                    }
+                }
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "title", true);
+                if (title == null) {
+                    title = new ArrayList<LocalizedString>();
+                }
+                title.add(s);
+                mode = 1;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "description")) {
+                if ((mode != 0) && (mode != 1) && (mode != 2)) {
+                    if (strict) {
+                        throw new XMLStreamException(
+                                "element '" + new QName(ns, "description") +
+                                        "' not allowed here [required content" +
+                                        " model is: title*, description*, " +
+                                        "(author | contact | extent | history" +
+                                        " | langUsage | restrictions | " +
+                                        "subjects | links | implementation)*]",
+                                        reader.getLocation());
+                    } else {
+                        logger.warn("element '{}' not allowed here [required " +
+                                "content model is: title*, description*, " +
+                                "(author | contact | extent | history | " +
+                                "langUsage | restrictions | subjects | links " +
+                                "| implementation)*] at position " +
+                                "[row, col]: [{}, {}]",
+                                new QName(ns, "description"),
+                                reader.getLocation().getLineNumber(),
+                                reader.getLocation().getColumnNumber());
+                    }
+                }
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "description", true);
+                if (description == null) {
+                    description = new ArrayList<LocalizedString>();
+                }
+                description.add(s);
+                mode = 2;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "author")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "author", true);
+                if (author == null) {
+                    author = new ArrayList<LocalizedString>();
+                }
+                author.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "contact")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "contact", true);
+                if (contact == null) {
+                    contact = new ArrayList<LocalizedString>();
+                }
+                contact.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "extent")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "extent", true);
+                if (extent == null) {
+                    extent = new ArrayList<LocalizedString>();
+                }
+                extent.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "history")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "history", true);
+                if (history == null) {
+                    history = new ArrayList<LocalizedString>();
+                }
+                history.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader,
+                    ns, "restrictions")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "restrictions", true);
+                if (restrictions == null) {
+                    restrictions = new ArrayList<LocalizedString>();
+                }
+                restrictions.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "subjects")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "subjects", true);
+                if (subjects == null) {
+                    subjects = new ArrayList<LocalizedString>();
+                }
+                subjects.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "links")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "links", true);
+                if (links == null) {
+                    links = new ArrayList<LocalizedString>();
+                }
+                links.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader,
+                    ns, "implementation")) {
+                LocalizedString s =
+                        parseStringI18N(reader, strict, ns, "implementation", true);
+                if (implementation == null) {
+                    implementation = new ArrayList<LocalizedString>();
+                }
+                implementation.add(s);
+                mode = 3;
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "langUsage")) {
+                // FIXME: implement!
+                XmlStreamReaderUtils.readContent(reader, ns, "langUsage", false);
+                mode = 3;
+            } else {
+                break;
+            }
+        } // for
+
+        return new DatabaseInfo(title, description, author, contact,
+                extent, history, langUsage, restrictions, subjects,
+                links, implementation);
+    }
+
+
+    private static IndexInfo parseIndexInfo(XMLStreamReader reader,
+            boolean strict, String ns) throws XMLStreamException,
+            SRUClientException {
+        /*
+         * indexInfo ((set | index | sortKeywords)+)
+         */
+        boolean found = false;
+        List<IndexInfo.Set> sets = null;
+        List<IndexInfo.Index> indexes = null;
+        for (;;) {
+            if (XmlStreamReaderUtils.readStart(reader, ns, "set", false, true)) {
+                final String identifier =
+                        XmlStreamReaderUtils.readAttributeValue(reader,
+                                null, "identifier");
+                final String name =
+                        XmlStreamReaderUtils.readAttributeValue(reader,
+                                null, "name");
+                XmlStreamReaderUtils.consumeStart(reader);
+
+                // indexInfo/set/title
+                List<LocalizedString> titles = null;
+                for (;;) {
+                    LocalizedString title =
+                            parseStringI18N(reader, strict, ns, "title", false);
+                    if (title == null) {
+                        break;
+                    }
+                    if (titles == null) {
+                        titles = new ArrayList<LocalizedString>();
+                    }
+                    titles.add(title);
+                } // for
+                XmlStreamReaderUtils.readEnd(reader, ns, "set");
+
+                if (sets == null) {
+                    sets = new ArrayList<IndexInfo.Set>();
+                }
+                sets.add(new IndexInfo.Set(identifier, name, titles));
+                found = true;
+            } else if (XmlStreamReaderUtils.readStart(reader, ns,
+                    "index", false, true)) {
+                final String id =
+                        XmlStreamReaderUtils.readAttributeValue(reader, null, "id");
+                final boolean can_search = parseBooleanAttribute(reader,
+                        strict, null, "search", false);
+                final boolean can_scan = parseBooleanAttribute(reader,
+                        strict, null, "scan", false);
+                final boolean can_sort = parseBooleanAttribute(reader,
+                        strict, null, "sort", false);
+                XmlStreamReaderUtils.consumeStart(reader);
+
+                // indexInfo/index/title
+                List<LocalizedString> titles = null;
+                for (;;) {
+                    LocalizedString title =
+                            parseStringI18N(reader, strict, ns, "title", false);
+                    if (title == null) {
+                        break;
+                    }
+                    if (titles == null) {
+                        titles = new ArrayList<LocalizedString>();
+                    }
+                    titles.add(title);
+                } // for
+
+                // indexInfo/index/map ((attr+)|name)
+                List<IndexInfo.Index.Map> maps = null;
+                boolean first_map = true;
+                while (XmlStreamReaderUtils.readStart(reader, ns, "map",
+                        first_map, true)) {
+                    final boolean primary = parseBooleanAttribute(reader,
+                            strict, null, "primary", false);
+                    XmlStreamReaderUtils.consumeStart(reader);
+
+                    if (XmlStreamReaderUtils.peekStart(reader, ns, "attr")) {
+                        /*
+                         * skip "attr" elements, because they are not supported
+                         */
+                        while (XmlStreamReaderUtils.readStart(reader,
+                                ns, "attr", false)) {
+                            logger.debug("skipping 'attr'");
+                            XmlStreamReaderUtils.readEnd(reader, ns, "attr");
+                        } // while (attr)
+                    } else if (XmlStreamReaderUtils.peekStart(reader,
+                            ns, "name")) {
+                        XmlStreamReaderUtils.readStart(reader, ns, "name", true, true);
+                        final String set =
+                                XmlStreamReaderUtils.readAttributeValue(reader,
+                                        null, "set");
+                        // FIXME: check 'set'
+                        XmlStreamReaderUtils.consumeStart(reader);
+                        final String name =
+                                XmlStreamReaderUtils.readString(reader, false);
+                        // FIXME: check 'name'
+                        XmlStreamReaderUtils.readEnd(reader, ns, "name");
+                        if ((set != null) && (name != null)) {
+                            if (maps == null) {
+                                maps = new ArrayList<IndexInfo.Index.Map>();
+                            }
+                            maps.add(new IndexInfo.Index.Map(primary, set, name));
+                        }
+                    } else {
+                        // FIXME: error message
+                        throw new XMLStreamException(
+                                "expected 'attr' or 'name'",
+                                reader.getLocation());
+                    }
+
+                    XmlStreamReaderUtils.readEnd(reader, ns, "map");
+                    first_map = false;
+                } // while (map)
+
+                // indexInfo/index/configInfo (optional)
+                if (XmlStreamReaderUtils.readStart(reader, ns, "configInfo", false)) {
+                    logger.debug("skipping 'configInfo' within 'indexInfo/index'");
+                    XmlStreamReaderUtils.readEnd(reader, ns, "configInfo", true);
+                }
+
+                XmlStreamReaderUtils.readEnd(reader, ns, "index");
+                if (indexes == null) {
+                    indexes = new ArrayList<IndexInfo.Index>();
+                }
+                indexes.add(new IndexInfo.Index(id, titles, can_search,
+                        can_scan, can_sort, maps));
+                found = true;
+            } else if (XmlStreamReaderUtils.readStart(reader, ns, "sortKeyword", false)) {
+                logger.debug("skipping 'sortKeywords'");
+                XmlStreamReaderUtils.readEnd(reader, ns, "sortKeyword", true);
+                found = true;
+            } else {
+                break;
+            }
+        } // for
+        if (!found) {
+            throw new XMLStreamException("expected at least one '" +
+                    new QName(ns, "set") + "', '" +
+                    new QName(ns, "index") + "' or '" +
+                    new QName(ns, "sortKeyword") +
+                    "' element within element '" +
+                    new QName(ns, "indexInfo") + "'",
+                    reader.getLocation());
+        }
+        return ((sets != null) || (indexes != null))
+                ? new IndexInfo(sets, indexes)
+                : null;
+    }
+
+
+    private static ConfigInfo parseConfigInfo(XMLStreamReader reader,
+            boolean strict, String ns) throws XMLStreamException,
+            SRUClientException {
+        /*
+         * configInfo ((default|setting|supports)*)
+         */
+        Map<String, String> defaults = null;
+        Map<String, String> settings = null;
+        Map<String, String> supports = null;
+        for (;;) {
+            if (XmlStreamReaderUtils.peekStart(reader, ns, "default")) {
+                defaults = parseConfigInfoItem(reader, strict, ns,
+                        "default", defaults);
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "setting")) {
+                settings = parseConfigInfoItem(reader, strict, ns,
+                        "setting", settings);
+            } else if (XmlStreamReaderUtils.peekStart(reader, ns, "supports")) {
+                supports = parseConfigInfoItem(reader, strict, ns,
+                        "supports", supports);
+            } else {
+                break;
+            }
+        } // for
+
+        return ((defaults != null) || (settings != null) || (supports != null))
+                ? new ConfigInfo(defaults, settings, supports)
+                : null;
+    }
+
+
+    private static LocalizedString parseStringI18N(XMLStreamReader reader,
+            boolean strict, String ns, String localName, boolean required)
+            throws XMLStreamException {
+        if (!XmlStreamReaderUtils.readStart(reader, ns, localName, required,
+                true)) {
+            return null;
+        }
+        final String lang = XmlStreamReaderUtils.readAttributeValue(reader,
+                null, "lang", false);
+        if (lang != null) {
+            if (lang.length() != 2) {
+                // FIXME: message
+                logger.warn("ZeeRex sugguests to use 2-letter codes");
+            }
+        }
+        final boolean primary =
+                parseBooleanAttribute(reader, strict, null, "primary", false);
+        XmlStreamReaderUtils.consumeStart(reader);
+        String value = XmlStreamReaderUtils.readString(reader, false);
+        XmlStreamReaderUtils.readEnd(reader, ns, localName);
+        return new LocalizedString(value, lang, primary);
+    }
+
+
+    private static boolean parseBooleanAttribute(XMLStreamReader reader,
+            boolean strict, String ns, String localName, boolean defaultValue)
+            throws XMLStreamException {
+        boolean result = defaultValue;
+        final String s = XmlStreamReaderUtils.readAttributeValue(reader,
+                ns, localName, false);
+        if (s != null) {
+            if (STRING_TRUE.equalsIgnoreCase(s)) {
+                if (!STRING_TRUE.equals(s)) {
+                    if (strict) {
+                        throw new XMLStreamException("capitalization");
+                    } else {
+                        // FIXME: message
+                        logger.warn("capitalizaton");
+                    }
+                }
+                result = true;
+            } else if (PRIMARY_FALSE.equalsIgnoreCase(s)) {
+                if (!PRIMARY_FALSE.equals(s)) {
+                    if (strict) {
+                        throw new XMLStreamException("capitalization");
+                    } else {
+                        // FIXME: message
+                        logger.warn("capitalizaton");
+                    }
+                }
+                result = false;
+            } else {
+                // FIXME: message
+                throw new XMLStreamException("value of @" + localName + " invalid");
+            }
+        }
+        return result;
+    }
+
+
+    private static List<Schema> parseSchemaInfo(XMLStreamReader reader,
+            boolean strict, String ns) throws XMLStreamException {
+        /*
+         * schemaInfo (schema+)
+         * schema (title*)
+         */
+        List<Schema> schemaInfo = null;
+        boolean first_schema = true;
+        while (XmlStreamReaderUtils.readStart(reader, ns, "schema",
+                first_schema, true)) {
+            final String identifier =
+                    XmlStreamReaderUtils.readAttributeValue(reader,
+                            null, "identifier", true);
+            final String name = XmlStreamReaderUtils.readAttributeValue(reader,
+                    null, "name", true);
+            final String location =
+                    XmlStreamReaderUtils.readAttributeValue(reader,
+                            null, "location");
+            final boolean sort = parseBooleanAttribute(reader, strict,
+                    null, "sort", false);
+            final boolean retrieve = parseBooleanAttribute(reader, strict,
+                    null, "retrieve", true);
+            XmlStreamReaderUtils.consumeStart(reader);
+
+            List<LocalizedString> titles = null;
+            for (;;) {
+                LocalizedString title =
+                        parseStringI18N(reader, strict, ns, "title", false);
+                if (title == null) {
+                    break;
+                }
+                if (titles == null) {
+                    titles = new ArrayList<LocalizedString>();
+                }
+                titles.add(title);
+            } // for
+            XmlStreamReaderUtils.readEnd(reader, ns, "schema");
+
+            if (schemaInfo == null) {
+                schemaInfo = new ArrayList<Schema>();
+            }
+            schemaInfo.add(new Schema(identifier, name, location, sort,
+                    retrieve, titles));
+            first_schema = false;
+        }
+        return schemaInfo;
+    }
+
+
+    private static Map<String, String> parseConfigInfoItem(
+            XMLStreamReader reader, boolean strict, String ns,
+            String localName, Map<String, String> map)
+            throws XMLStreamException {
+        XmlStreamReaderUtils.readStart(reader, ns, localName, true, true);
+        final String type = XmlStreamReaderUtils.readAttributeValue(reader,
+                null, "type", true);
+        XmlStreamReaderUtils.consumeStart(reader);
+        final String value = XmlStreamReaderUtils.readString(reader, true);
+        XmlStreamReaderUtils.readEnd(reader, ns, localName);
+
+        if ((type != null) && (value != null)) {
+            if (map == null) {
+                map = new HashMap<String, String>();
+            }
+            if (map.containsKey(type)) {
+                logger.warn(
+                        "{} with type '{}' is already present, skipping duplicate entry",
+                        localName, type);
+            } else {
+                map.put(type, value);
+            }
+        }
+        return map;
+    }
+
+} // class SRUExplainRecordDataParser
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRequest.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRequest.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainRequest.java	(revision 2998)
@@ -0,0 +1,128 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.net.URI;
+
+/**
+ * An object for performing a <em>explain</em> operation.
+ *
+ * @see SRUExplainHandler
+ * @see <a href="http://www.loc.gov/standards/sru/specs/explain.html">SRU Explain Operation</a>
+ */
+public class SRUExplainRequest extends SRUAbstractRequest {
+    private SRURecordPacking recordPacking;
+    private boolean parseRecordDataEnabled = false;
+
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the endpoint
+     */
+    public SRUExplainRequest(URI baseURI) {
+        super(baseURI);
+    }
+
+
+    /**
+     * Constructor.
+     * 
+     * @param baseURI
+     *            the baseURI of the endpoint
+     */
+    public SRUExplainRequest(String baseURI) {
+        super(baseURI);
+    }
+
+
+    /**
+     * Set the requested record packing.
+     * 
+     * @param recordPacking
+     *            the requested record packing
+     * @see SRURecordPacking
+     */
+    public void setRecordPacking(SRURecordPacking recordPacking) {
+        if (recordPacking == null) {
+            throw new NullPointerException("recordPacking == null");
+        }
+        this.recordPacking = recordPacking;
+    }
+
+
+    /**
+     * Get the requested record packing.
+     *
+     * @return the requested record packing
+     * @see SRURecordPacking
+     */
+    public SRURecordPacking getRecordPacking() {
+        return recordPacking;
+    }
+
+
+    /**
+     * Enable or disable parsing of explain record data (ZeeRex record) of the
+     * explain response.
+     * 
+     * @param enabled
+     *            <code>true</code> enabled parsing, <code>false</code> disables
+     *            parsing
+     */
+    public void setParseRecordDataEnabled(boolean enabled) {
+        this.parseRecordDataEnabled = enabled;
+    }
+    
+
+    /**
+     * Check, whether the record data of a explain response (ZeeRex record)
+     * shall be parsed or not.
+     * 
+     * @return <code>true</code> if parsing is enabled, <code>false</code>
+     *         otherwise
+     */
+    public boolean isParseRecordDataEnabled() {
+        return parseRecordDataEnabled;
+    }
+
+
+    @Override
+    SRUOperation getOperation() {
+        return SRUOperation.EXPLAIN;
+    }
+
+
+    @Override
+    void addParametersToURI(URIHelper uriHelper) throws SRUClientException {
+        // recordPacking
+        if (recordPacking != null) {
+            switch (recordPacking) {
+            case XML:
+                uriHelper.append(PARAM_RECORD_PACKING, RECORD_PACKING_XML);
+                break;
+            case STRING:
+                uriHelper.append(PARAM_RECORD_PACKING, RECORD_PACKING_STRING);
+                break;
+            default:
+                throw new SRUClientException("unsupported record packing: " +
+                        recordPacking);
+            } // switch
+        }
+    }
+
+} // class SRUExplainRequest
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainResponse.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainResponse.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUExplainResponse.java	(revision 2998)
@@ -0,0 +1,68 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import org.w3c.dom.DocumentFragment;
+
+
+
+/**
+ * A response to a <em>explain</em> request.
+ */
+public final class SRUExplainResponse extends
+        SRUAbstractResponse<SRUExplainRequest> {
+    private final SRURecord record;
+
+
+    SRUExplainResponse(SRUExplainRequest request,
+            List<SRUDiagnostic> diagnostics,
+            DocumentFragment extraResponseData,
+            int totalBytesTransferred,
+            long timeTotal,
+            long timeQueued,
+            long timeNetwork,
+            long timeProcessing,
+            SRURecord record) {
+        super(request, diagnostics, extraResponseData, totalBytesTransferred,
+                timeTotal, timeQueued, timeNetwork, timeProcessing);
+        this.record = record;
+    }
+
+
+    /**
+     * Get the explain record.
+     *
+     * @return the explain record or <code>null</code>
+     */
+    public SRURecord getRecord() {
+        return record;
+    }
+
+
+    /**
+     * Check, if response contains a record.
+     *
+     * @return <code>true</code> of response contains a record,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasRecord() {
+        return record != null;
+    }
+
+} // class SRUExplainResponse
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecord.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecord.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecord.java	(revision 2998)
@@ -0,0 +1,131 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import org.w3c.dom.DocumentFragment;
+
+
+/**
+ * Class for holding a single record from a result set.
+ *
+ * @see SRUSearchRetrieveResponse
+ */
+public final class SRURecord {
+    private final SRURecordData recordData;
+    private final String recordIdentifier;
+    private final int recordPosition;
+    private DocumentFragment extraRecordData = null;
+
+
+    SRURecord(SRURecordData recordData, String recordIdentifier,
+            int recordPosition) {
+        if (recordData == null) {
+            throw new NullPointerException("recordData == null");
+        }
+        this.recordData = recordData;
+        this.recordIdentifier = recordIdentifier;
+        this.recordPosition = recordPosition;
+    }
+
+
+    /**
+     * The record schema for this record.
+     *
+     * @return the record schema for this record
+     */
+    public String getRecordSchema() {
+        return recordData.getRecordSchema();
+    }
+
+
+    /**
+     * Check if this record is in a certain record schema.
+     *
+     * @param recordSchema
+     *            the record schema to test against
+     * @return <code>true</code> if the record is in the supplied record schema,
+     *         <code>false</code> otherwise
+     * @throws NullPointerException
+     *             if any required arguments are not supplied
+     */
+    public boolean isRecordSchema(String recordSchema) {
+        if (recordSchema == null) {
+            throw new NullPointerException("recordSchema == null");
+        }
+        return recordData.getRecordSchema().equals(recordSchema);
+    }
+
+
+    /**
+     * Get the record.
+     *
+     * @return the record
+     */
+    public SRURecordData getRecordData() {
+        return recordData;
+    }
+
+
+    /**
+     * Get the record identifier (only SRU version 1.2).
+     *
+     * @return the record identifier or <code>null</code> if not available
+     */
+    public String getRecordIdentifier() {
+        return recordIdentifier;
+    }
+
+
+    /**
+     * Get the record position in the result set.
+     *
+     * @return position of the record in the result set or <code>-1</code> if
+     *         not available
+     */
+    public int getRecordPosition() {
+        return recordPosition;
+    }
+
+
+    /**
+     * Get extra record data attached to this record.
+     *
+     * @return get an instance of {@link DocumentFragment} containing the XML
+     *         fragment for the extra record data from the SRU response or
+     *         <code>null</code> if none are available
+     */
+    public DocumentFragment getExtraRecordData() {
+        return extraRecordData;
+    }
+
+
+    /**
+     * Check, if this record has extra record data attached to it.
+     *
+     * @return <code>true</code> if extra record data is attached,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasExtraRecordData() {
+        return extraRecordData != null;
+    }
+
+
+    void setExtraRecordData(DocumentFragment extraRecordData) {
+        this.extraRecordData = extraRecordData;
+    }
+
+} // class SRURecord
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordData.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordData.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordData.java	(revision 2998)
@@ -0,0 +1,42 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * Interface for parsed record data.
+ * 
+ */
+public interface SRURecordData {
+
+    /**
+     * This record is transient. If you want to store the data to process it
+     * later, you need to create a copy of the data.
+     * 
+     * @return <code>true</code>, if record is transient,
+     *         <code>false</code> otherwise
+     */
+    public boolean isTransient();
+
+
+    /**
+     * The record schema for this record.
+     * 
+     * @return the record schema
+     */
+    public String getRecordSchema();
+
+} // interface SRURecordData
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordDataParser.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordDataParser.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordDataParser.java	(revision 2998)
@@ -0,0 +1,51 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+
+/**
+ * A parser to parse record data and create appropriate Java objects.
+ *
+ */
+public interface SRURecordDataParser {
+    /**
+     * The record schema this parser is able to process.
+     *
+     * @return the record schema this parser is able to process
+     */
+    public String getRecordSchema();
+
+
+    /**
+     * Parse a record data into a Java object.
+     *
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the record data
+     * @return the parsed record
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRURecordData
+     */
+    public SRURecordData parse(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException;
+
+} // interface SRURecordDataParser
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordPacking.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordPacking.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRURecordPacking.java	(revision 2998)
@@ -0,0 +1,45 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * SRU record packing
+ */
+public enum SRURecordPacking {
+    /**
+     * XML record packing
+     */
+    XML {
+        @Override
+        String getStringValue() {
+            return "xml";
+        }
+    },
+
+    /**
+     * String record packing
+     */
+    STRING {
+        @Override
+        String getStringValue() {
+            return "string";
+        }
+    };
+
+    abstract String getStringValue();
+
+} // enum SRURecordPacking
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanHandler.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanHandler.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanHandler.java	(revision 2998)
@@ -0,0 +1,135 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+
+/**
+ * Receive notifications to the response of a <em>scan</em> request.
+ *
+ * @see SRUScanRequest
+ * @see <a href="http://www.loc.gov/standards/sru/specs/scan.html">SRU
+ *      Scan Operation</a>
+ */
+public interface SRUScanHandler {
+    /**
+     * Receive notification of diagnostics.
+     *
+     * @param diagnostics
+     *            a list of {@link SRUDiagnostic}
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRUDiagnostic
+     */
+    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of request statistics.
+     *
+     * @param totalBytesTransferred
+     *            the total number of bytes transferred while receiving the
+     *            response
+     * @param millisTotal
+     *            the total time spend processing the request
+     * @param millisNetwork
+     *            the time spend performing network operations
+     * @param millisProcessing
+     *            the time spend processing the response
+     */
+    public void onRequestStatistics(int totalBytesTransferred,
+            long millisTotal, long millisNetwork, long millisProcessing);
+
+
+    /**
+     * Receive notification of extra response data.
+     *
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the extra response data
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see <a href="http://www.loc.gov/standards/sru/specs/extra-data.html">SRU
+     *      Extra Data / Extensions</a>
+     */
+    public void onExtraResponseData(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException;
+
+
+    /**
+     * Receive notifications of the start of the enumeration of terms in the
+     * response.
+     *
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onStartTerms() throws SRUClientException;
+
+
+    /**
+     * Receive notifications of the end of the enumeration of terms in the
+     * response.
+     *
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onFinishTerms() throws SRUClientException;
+
+
+    /**
+     * Receive notification of a term.
+     *
+     * @param value
+     *            the term (exactly) as it appears in the index
+     * @param numberOfRecords
+     *            the number of records for the current term which would be
+     *            matched
+     * @param displayTerm
+     *            a string for the current term to display to the end user in
+     *            place of the term itself or <code>null</code> if not available
+     * @param whereInList
+     *            a flag to indicate the position of the term within the
+     *            complete term list or <code>null</code> of not available
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onTerm(String value, int numberOfRecords, String displayTerm,
+            SRUWhereInList whereInList) throws SRUClientException;
+
+
+    /**
+     * Receive notification of extra term data.
+     *
+     * @param value
+     *            the term (exactly) as it appears in the index
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the extra term data
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onExtraTermData(String value, XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException;
+
+} // interface SRUScanHandler
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanRequest.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanRequest.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanRequest.java	(revision 2998)
@@ -0,0 +1,204 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.net.URI;
+
+/**
+ * An object for performing a <em>explain</em> operation.
+ * <p>The following argument arguments are mandatory:</p>
+ * <ul>
+ *   <li><em>scanClause</em></li>
+ * </ul>
+ *
+ * @see SRUScanHandler
+ * @see <a href="http://www.loc.gov/standards/sru/specs/scan.html">SRU Scan
+ *      Operation</a>
+ */
+public class SRUScanRequest extends SRUAbstractRequest {
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_SCAN_CLAUSE =
+            "x-malformed-scanClause";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_RESPONSE_POSITION =
+            "x-malformed-responsePosition";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_MAXIMUM_TERMS =
+            "x-malformed-maximumTerms";
+    private String scanClause;
+    private int responsePosition = -1;
+    private int maximumTerms = -1;
+
+
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the endpoint
+     */
+    public SRUScanRequest(URI baseURI) {
+        super(baseURI);
+    }
+
+    
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the endpoint
+     */
+    public SRUScanRequest(String baseURI) {
+        super(baseURI);
+    }
+
+
+    /**
+     * Get the value of the <em>scanClause</em> argument for this request.
+     *
+     * @return the value for the <em>scanClause</em> argument or
+     *         <code>null</code> of none was set
+     */
+    public String getScanClause() {
+        return scanClause;
+    }
+
+
+    /**
+     * Set the value of the <em>scanClause</em> argument for this request.
+     *
+     * @param scanClause
+     *            the value for the <em>scanClause</em> argument
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setScanClause(String scanClause) {
+        if (scanClause == null) {
+            throw new NullPointerException("scanClause == null");
+        }
+        if (scanClause.isEmpty()) {
+            throw new IllegalArgumentException("scanClause is an empty string");
+        }
+        this.scanClause = scanClause;
+    }
+
+
+    /**
+     * Get the value of the <em>responsePosition</em> argument for this request.
+     *
+     * @return the value for the <em>responsePosition</em> argument
+     */
+    public int getResponsePosition() {
+        return responsePosition;
+    }
+
+
+    /**
+     * Set the value of the <em>responsePosition</em> argument for this request.
+     *
+     * @param responsePosition
+     *            the value for the <em>responsePosition</em> argument
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setResponsePosition(int responsePosition) {
+        if (responsePosition < 0) {
+            throw new IllegalArgumentException("responsePosition < 0");
+        }
+        this.responsePosition = responsePosition;
+    }
+
+
+    /**
+     * Get the value of the <em>maximumTerms</em> argument for this request.
+     *
+     * @return the value for the <em>maximumTerms</em> argument
+     */
+    public int getMaximumTerms() {
+        return maximumTerms;
+    }
+
+
+    /**
+     * Set the value of the <em>maximumTerms</em> argument for this request.
+     *
+     * @param maximumTerms
+     *            the value for the <em>maximumTerms</em> argument
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setMaximumTerms(int maximumTerms) {
+        if (maximumTerms < 0) {
+            throw new IllegalArgumentException("maximumTerms < 0");
+        }
+        this.maximumTerms = maximumTerms;
+    }
+
+
+    @Override
+    SRUOperation getOperation() {
+        return SRUOperation.SCAN;
+    }
+
+
+    @Override
+    void addParametersToURI(URIHelper uriHelper) throws SRUClientException {
+        // scanClause
+        final String malformedScan =
+                getExtraRequestData(X_MALFORMED_SCAN_CLAUSE);
+        if (malformedScan == null) {
+            if ((scanClause == null) || scanClause.isEmpty()) {
+                throw new SRUClientException(
+                        "mandatory argument 'scanClause' not set or empty");
+            }
+            uriHelper.append(PARAM_SCAN_CLAUSE, scanClause);
+        } else {
+            if (!malformedScan.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_VERSION, malformedScan);
+            }
+        }
+
+        // responsePosition
+        final String malformedResponsePosition =
+                getExtraRequestData(X_MALFORMED_RESPONSE_POSITION);
+        if (malformedResponsePosition == null) {
+            if (responsePosition > -1) {
+                uriHelper.append(PARAM_RESPONSE_POSITION, responsePosition);
+            }
+        } else {
+            if (!malformedResponsePosition.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_RESPONSE_POSITION,
+                        malformedResponsePosition);
+            }
+        }
+
+        // maximumTerms
+        final String malformedMaximumTerms =
+                getExtraRequestData(X_MALFORMED_MAXIMUM_TERMS);
+        if (malformedMaximumTerms == null) {
+            if (maximumTerms > -1) {
+                uriHelper.append(PARAM_MAXIMUM_TERMS, maximumTerms);
+            }
+        } else {
+            if (!malformedMaximumTerms.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_MAXIMUM_TERMS, malformedMaximumTerms);
+            }
+        }
+    }
+
+} // class SRUScanRequest
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanResponse.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanResponse.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUScanResponse.java	(revision 2998)
@@ -0,0 +1,71 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.w3c.dom.DocumentFragment;
+
+
+
+/**
+ * A response to a <em>scan</em> request.
+ */
+public final class SRUScanResponse extends SRUAbstractResponse<SRUScanRequest> {
+    private final List<SRUTerm> terms;
+
+
+    SRUScanResponse(SRUScanRequest request,
+            List<SRUDiagnostic> diagnostics,
+            DocumentFragment extraResponseData,
+            int totalBytesTransferred,
+            long timeTotal,
+            long timeQueued,
+            long timeNetwork,
+            long timeProcessing,
+            List<SRUTerm> terms) {
+        super(request, diagnostics, extraResponseData, totalBytesTransferred,
+                timeTotal, timeQueued, timeNetwork, timeProcessing);
+        this.terms = ((terms != null) && !terms.isEmpty())
+                ? Collections.unmodifiableList(terms)
+                : null;
+    }
+
+
+    /**
+     * Get list of terms matched by the request.
+     *
+     * @return a list of terms or <code>null</code> if no terms matched the
+     *         request.
+     */
+    public List<SRUTerm> getTerms() {
+        return terms;
+    }
+
+
+    /**
+     * Check, if response contains any terms.
+     *
+     * @return <code>true</code> of response contains terms, <code>false</code>
+     *         otherwise
+     */
+    public boolean hasTerms() {
+        return terms != null;
+    }
+
+} // class SRUScanResponse
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveHandler.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveHandler.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveHandler.java	(revision 2998)
@@ -0,0 +1,166 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+
+/**
+ * Receive notifications to the response of a <em>searchRetrieve</em> request.
+ *
+ * @see SRUSearchRetrieveRequest
+ * @see <a href="http://www.loc.gov/standards/sru/specs/search-retrieve.html">
+ *      SRU SearchRetrieve Operation</a>
+ */
+public interface SRUSearchRetrieveHandler {
+
+    /**
+     * Receive notification of diagnostics.
+     *
+     * @param diagnostics
+     *            a list of {@link SRUDiagnostic}
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRUDiagnostic
+     */
+    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of request statistics.
+     *
+     * @param totalBytesTransferred
+     *            the total number of bytes transferred while receiving the
+     *            response
+     * @param millisTotal
+     *            the total time spend processing the request
+     * @param millisNetwork
+     *            the time spend performing network operations
+     * @param millisProcessing
+     *            the time spend processing the response
+     */
+    public void onRequestStatistics(int totalBytesTransferred,
+            long millisTotal, long millisNetwork, long millisProcessing);
+
+
+    /**
+     * Receive notification of extra response data.
+     *
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the extra response data
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see <a href="http://www.loc.gov/standards/sru/specs/extra-data.html">SRU
+     *      Extra Data / Extensions</a>
+     */
+    public void onExtraResponseData(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException;
+
+
+    /**
+     * Receive notifications of the start of the enumeration of records in the
+     * response.
+     *
+     * @param numberOfRecords
+     *            the number of records or <code>-1</code> if not available
+     * @param resultSetId
+     *            the result set id or <code>null</code> if not available
+     * @param resultSetIdleTime
+     *            the result set idle time or <code>-1</code> if not available
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onStartRecords(int numberOfRecords, String resultSetId,
+            int resultSetIdleTime) throws SRUClientException;
+
+
+    /**
+     * Receive notifications of the end of the enumeration of records in the
+     * response.
+     *
+     * @param nextRecordPosition
+     *            the next record position or <code>-1</code> if not available
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onFinishRecords(int nextRecordPosition)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of a record in the result set.
+     *
+     * @param identifier
+     *            identifier of the record or <code>null</code> if not available
+     * @param position
+     *            position of the record in the result set or <code>-1</code> if
+     *            not available
+     * @param data
+     *            the parsed record data
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRURecordData
+     * @see SRURecordDataParser
+     */
+    public void onRecord(String identifier, int position, SRURecordData data)
+            throws SRUClientException;
+
+
+    /**
+     * Receive notification of a surrogate record in the result set.
+     *
+     * @param identifier
+     *            identifier of the record or <code>null</code> if not available
+     * @param position
+     *            position of the record in the result set or <code>-1</code> if
+     *            not available
+     * @param data
+     *            the surrogate record data, i.e. a diagnostic
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see SRUDiagnostic
+     */
+    public void onSurrogateRecord(String identifier, int position,
+            SRUDiagnostic data) throws SRUClientException;
+
+
+    /**
+     * Receive notification of extra record data.
+     *
+     * @param identifier
+     *            identifier of the record or <code>null</code> if not available
+     * @param position
+     *            position of the record in the result set or <code>-1</code> if
+     *            not available
+     * @param reader
+     *            a {@link XMLStreamReader} to parse the extra term data
+     * @throws XMLStreamException
+     *             an error occurred while parsing the response
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     */
+    public void onExtraRecordData(String identifier, int position,
+            XMLStreamReader reader) throws XMLStreamException,
+            SRUClientException;
+
+} // interface SRUSearchRetrieveHandler
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveRequest.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveRequest.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveRequest.java	(revision 2998)
@@ -0,0 +1,356 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.net.URI;
+
+/**
+ * An object for performing a <em>explain</em> operation.
+ * <p>
+ * The following argument arguments are mandatory:
+ * </p>
+ * <ul>
+ * <li><em>query</em></li>
+ * </ul>
+ *
+ * @see SRUSearchRetrieveHandler
+ * @see <a href="http://www.loc.gov/standards/sru/specs/search-retrieve.html">
+ *      SRU SearchRetrieve Operation</a>
+ */
+public class SRUSearchRetrieveRequest extends SRUAbstractRequest {
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_QUERY =
+            "x-malformed-query";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_START_RECORD =
+            "x-malformed-startRecord";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_MAXIMUM_RECORDS =
+            "x-malformed-maximumRecords";
+    /** for end-point conformance testing only. never use in production. */
+    public static final String X_MALFORMED_RECORD_PACKING =
+            "x-malformed-recordPacking";
+    private String query;
+    private int startRecord = -1;
+    private int maximumRecords = -1;
+    private SRURecordPacking recordPacking;
+    private String recordSchema;
+    private int resultSetTTL = -1;
+
+
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the endpoint
+     */
+    public SRUSearchRetrieveRequest(URI baseURI) {
+        super(baseURI);
+    }
+
+
+    /**
+     * Constructor.
+     *
+     * @param baseURI
+     *            the baseURI of the endpoint
+     */
+    public SRUSearchRetrieveRequest(String baseURI) {
+        super(baseURI);
+    }
+
+
+    /**
+     * Get the value of the <em>query</em> argument for this request.
+     *
+     * @return the value for the <em>query</em> argument or <code>null</code> of
+     *         none was set
+     */
+    public String getQuery() {
+        return query;
+    }
+
+
+    /**
+     * Set the value of the <em>query</em> argument for this request.
+     *
+     * @param query
+     *            the value for the <em>query</em> argument
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setQuery(String query) {
+        if (query == null) {
+            throw new NullPointerException("query == null");
+        }
+        if (query.isEmpty()) {
+            throw new IllegalArgumentException("query is an empty string");
+        }
+        this.query = query;
+    }
+
+
+    /**
+     * Get the value of the <em>startRecord</em> argument for this request.
+     *
+     * @return the value for the <em>startRecord</em> argument or
+     *         <code>-1</code> of none was set
+     */
+    public int getStartRecord() {
+        return startRecord;
+    }
+
+
+    /**
+     * Set the value of the <em>startRecord</em> argument for this request.
+     *
+     * @param startRecord
+     *            the value for the <em>startRecord</em> argument
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setStartRecord(int startRecord) {
+        if (startRecord < 1) {
+            throw new IllegalArgumentException("startRecord < 1");
+        }
+        this.startRecord = startRecord;
+    }
+
+
+    /**
+     * Get the value of the <em>maximumRecords</em> argument for this request.
+     *
+     * @return the value for the <em>maximumRecords</em> argument or
+     *         <code>-1</code> of none was set
+     */
+    public int getMaximumRecords() {
+        return maximumRecords;
+    }
+
+
+    /**
+     * Set the value of the <em>maximumRecords</em> argument for this request.
+     *
+     * @param maximumRecords
+     *            the value for the <em>maximumRecords</em> argument
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setMaximumRecords(int maximumRecords) {
+        if (maximumRecords < 0) {
+            throw new IllegalArgumentException("maximumRecords < 0");
+        }
+        this.maximumRecords = maximumRecords;
+    }
+
+
+    /**
+     * Get the value of the <em>recordSchema</em> argument for this request.
+     *
+     * @return the value for the <em>recordSchema</em> argument or
+     *         <code>null</code> of none was set
+     */
+    public String getRecordSchema() {
+        return recordSchema;
+    }
+
+
+    /**
+     * Set the value of the <em>recordSchema</em> argument for this request.
+     *
+     * @param recordSchema
+     *            the value for the <em>recordSchema</em> argument
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setRecordSchema(String recordSchema) {
+        this.recordSchema = recordSchema;
+    }
+
+
+    /**
+     * Get the value of the <em>recordSchema</em> argument for this request.
+     *
+     * @return the value for the <em>recordSchema</em> argument or
+     *         <code>null</code> of none was set
+     */
+    public SRURecordPacking getRecordPacking() {
+        return recordPacking;
+    }
+
+
+    /**
+     * Set the value of the <em>recordPacking</em> argument for this request.
+     *
+     * @param recordPacking
+     *            the value for the <em>recordPacking</em> argument
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     */
+    public void setRecordPacking(SRURecordPacking recordPacking) {
+        if (recordPacking == null) {
+            throw new NullPointerException("recordPacking == null");
+        }
+        this.recordPacking = recordPacking;
+    }
+
+
+    /**
+     * Get the value of the <em>resultSetTTL</em> argument for this request.
+     *
+     * @return the value for the <em>resultSetTTL</em> argument or
+     *         <code>-1</code> of none was set
+     */
+    public int getResultSetTTL() {
+        return resultSetTTL;
+    }
+
+
+    /**
+     * Set the value of the <em>resultSetTTL</em> argument for this request.
+     *
+     * @param resultSetTTL
+     *            the value for the <em>resultSetTTL</em> argument
+     * @throws IllegalArgumentException
+     *             if any argument is invalid
+     */
+    public void setResultSetTTL(int resultSetTTL) {
+        this.resultSetTTL = resultSetTTL;
+    }
+
+
+    @Override
+    SRUOperation getOperation() {
+        return SRUOperation.SEARCH_RETRIEVE;
+    }
+
+
+    @Override
+    void addParametersToURI(URIHelper uriHelper) throws SRUClientException {
+        /*
+         * append query argument (mandatory)
+         *
+         * NB: Setting "x-malformed-query" as an extra request parameter makes
+         * the client to send invalid requests. This is intended to use for
+         * testing endpoints for protocol conformance (i.e. provoke an error)
+         * and SHOULD NEVER be used in production!
+         */
+        final String malformedQuery = getExtraRequestData(X_MALFORMED_QUERY);
+        if (malformedQuery == null) {
+            if ((query == null) || query.isEmpty()) {
+                throw new SRUClientException(
+                        "mandatory argument 'query' not set or empty");
+            }
+            uriHelper.append(PARAM_QUERY, query);
+        } else {
+            if (!malformedQuery.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_QUERY, malformedQuery);
+            }
+        }
+
+        /*
+         * append startRecord argument (optional)
+         *
+         * NB: Setting "x-malformed-startRecord" as an extra request parameter
+         * makes the client to send invalid requests. This is intended to use
+         * for testing endpoints for protocol conformance (i.e. provoke an
+         * error) and SHOULD NEVER be used in production!
+         */
+        final String malformedStartRecord =
+                getExtraRequestData(X_MALFORMED_START_RECORD);
+        if (malformedStartRecord == null) {
+            if (startRecord > 0) {
+                uriHelper.append(PARAM_START_RECORD, startRecord);
+            }
+        } else {
+            if (!malformedStartRecord.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_START_RECORD, malformedStartRecord);
+            }
+        }
+
+        /*
+         * append maximumRecords argument (optional)
+         *
+         * NB: Setting "x-malformed-maximumRecords" as an extra request
+         * parameter makes the client to send invalid requests. This is
+         * intended to use for testing endpoints for protocol conformance
+         * (i.e. provoke an error) and SHOULD NEVER be used in production!
+         */
+        final String malformedMaxiumRecords =
+                getExtraRequestData(X_MALFORMED_MAXIMUM_RECORDS);
+        if (malformedMaxiumRecords == null) {
+            if (maximumRecords > -1) {
+                uriHelper.append(PARAM_MAXIMUM_RECORDS, maximumRecords);
+            }
+        } else {
+            if (!malformedMaxiumRecords.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_MAXIMUM_RECORDS,
+                        malformedMaxiumRecords);
+            }
+        }
+
+        /*
+         * append recordPacking argument (optional)
+         *
+         * NB: Setting "x-malformed-recordPacking" as an extra request
+         * parameter makes the client to send invalid requests. This is
+         * intended to use for testing endpoints for protocol conformance
+         * (i.e. provoke an error) and SHOULD NEVER be used in production!
+         */
+        final String malformedRecordPacking =
+                getExtraRequestData(X_MALFORMED_RECORD_PACKING);
+        if (malformedRecordPacking == null) {
+            if (recordPacking != null) {
+                switch (recordPacking) {
+                case XML:
+                    uriHelper.append(PARAM_RECORD_PACKING, RECORD_PACKING_XML);
+                    break;
+                case STRING:
+                    uriHelper.append(PARAM_RECORD_PACKING,
+                            RECORD_PACKING_STRING);
+                    break;
+                default:
+                    throw new SRUClientException(
+                            "unsupported record packing: " + recordPacking);
+                } // switch
+            }
+        } else {
+            if (!malformedRecordPacking.equalsIgnoreCase(MALFORMED_OMIT)) {
+                uriHelper.append(PARAM_RECORD_PACKING, malformedRecordPacking);
+            }
+        }
+
+        /*
+         * append recordSchema argument (optional)
+         */
+        if (recordSchema != null) {
+            uriHelper.append(PARAM_RECORD_SCHEMA, recordSchema);
+        }
+
+        /*
+         * append resultSetTTL argument (optional)
+         */
+        if (resultSetTTL > -1) {
+            uriHelper.append(PARAM_RESULT_SET_TTL, resultSetTTL);
+        }
+    }
+
+} // class SRUSearchRetrieveRequest
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveResponse.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveResponse.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSearchRetrieveResponse.java	(revision 2998)
@@ -0,0 +1,123 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.w3c.dom.DocumentFragment;
+
+
+
+/**
+ * A response to a <em>searchRetrieve</em> request.
+ */
+public final class SRUSearchRetrieveResponse extends
+        SRUAbstractResponse<SRUSearchRetrieveRequest> {
+    private final int numberOfRecords;
+    private final String resultSetId;
+    private final int resultSetIdleTime;
+    private final List<SRURecord> records;
+    private final int nextRecordPosition;
+
+
+    SRUSearchRetrieveResponse(SRUSearchRetrieveRequest request,
+            List<SRUDiagnostic> diagnostics,
+            DocumentFragment extraResponseData,
+            int totalBytesTransferred,
+            long timeTotal,
+            long timeQueued,
+            long timeNetwork,
+            long timeParsing,
+            int numberOfRecords,
+            String resultSetId,
+            int resultSetIdleTime,
+            List<SRURecord> records,
+            int nextRecordPosition) {
+        super(request, diagnostics, extraResponseData, totalBytesTransferred,
+                timeTotal, timeQueued, timeNetwork, timeParsing);
+        this.numberOfRecords = numberOfRecords;
+        this.resultSetId = resultSetId;
+        this.resultSetIdleTime = resultSetIdleTime;
+        this.records = (records != null && !records.isEmpty())
+                ? Collections.unmodifiableList(records)
+                : null;
+        this.nextRecordPosition = nextRecordPosition;
+    }
+
+
+    /**
+     * Get the number of records.
+     *
+     * @return the number of records or <code>-1</code> if not available
+     */
+    public int getNumberOfRecords() {
+        return numberOfRecords;
+    }
+
+
+    /**
+     * Get the result set id.
+     *
+     * @return the result set id or <code>-1</code> if not available
+     */
+    public String getResultSetId() {
+        return resultSetId;
+    }
+
+
+    /**
+     * Get the result set idle time.
+     *
+     * @return the result set idle time or <code>-1</code> if not available
+     */
+    public int getResultSetIdleTime() {
+        return resultSetIdleTime;
+    }
+
+
+    /**
+     * Get the list of records.
+     *
+     * @return the list of records or <code>null</code> if none
+     */
+    public List<SRURecord> getRecords() {
+        return records;
+    }
+
+
+    /**
+     * Check, if response contains any records.
+     *
+     * @return <code>true</code> of response contains records,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasRecords() {
+        return records != null;
+    }
+
+
+    /**
+     * Get the next record position.
+     *
+     * @return the next record position or <code>-1</code> if not available
+     */
+    public int getNextRecordPosition() {
+        return nextRecordPosition;
+    }
+
+} // class SRUSearchRetrieveResponse
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSimpleClient.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSimpleClient.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSimpleClient.java	(revision 2998)
@@ -0,0 +1,1364 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.HttpClientUtils;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * A simple client to perform SRU operations using callbacks. The application
+ * must provide the appropriate callbacks to receive the results of the
+ * operations.
+ * <p>
+ * This client is reusable but not thread-safe: the application may reuse a
+ * client object, but it may not be concurrently shared between multiple
+ * threads.
+ * </p>
+ * <p>
+ * This class is modeled after Java's SAX-API.
+ * </p>
+ *
+ * @see SRUExplainHandler
+ * @see SRUScanHandler
+ * @see SRUSearchRetrieveHandler
+ * @see SRUDefaultHandlerAdapter
+ */
+public class SRUSimpleClient {
+    /** default version the client will use, if not otherwise specified */
+    public static final SRUVersion DEFAULT_SRU_VERSION = SRUVersion.VERSION_1_2;
+    private static final String SRU_NS =
+            "http://www.loc.gov/zing/srw/";
+    private static final String SRU_DIAGNOSIC_NS =
+            "http://www.loc.gov/zing/srw/diagnostic/";
+    private static final String SRU_DIAGNOSTIC_RECORD_SCHEMA =
+            "info:srw/schema/1/diagnostics-v1.1";
+    private static final String VERSION_1_1 = "1.1";
+    private static final String VERSION_1_2 = "1.2";
+    private static final String RECORD_PACKING_XML = "xml";
+    private static final String RECORD_PACKING_STRING = "string";
+    private static final Logger logger =
+            LoggerFactory.getLogger(SRUSimpleClient.class);
+    private final SRUVersion defaultVersion;
+    private final Map<String, SRURecordDataParser> parsers;
+    private final HttpClient httpClient;
+    private final XmlStreamReaderProxy proxy = new XmlStreamReaderProxy();
+    private final SRUExplainRecordDataParser explainRecordParser =
+            new SRUExplainRecordDataParser();
+
+    /**
+     * Constructor. This constructor will create a <em>strict</em> client and
+     * use the default SRU version.
+     *
+     * @see #DEFAULT_SRU_VERSION
+     */
+    public SRUSimpleClient() {
+        this(DEFAULT_SRU_VERSION, new HashMap<String, SRURecordDataParser>());
+    }
+
+
+    /**
+     * Constructor. This constructor will create a <em>strict</em> client.
+     *
+     * @param defaultVersion
+     *            the default version to use for SRU requests; may be overridden
+     *            by individual requests
+     */
+    public SRUSimpleClient(SRUVersion defaultVersion) {
+        this(defaultVersion, new HashMap<String, SRURecordDataParser>());
+    }
+
+
+    /**
+     * Constructor.
+     *
+     * <p>
+     * For internal use only.
+     * </p>
+     *
+     * @param defaultVersion
+     *            the default version to use for SRU requests; may be overridden
+     *            by individual requests
+     * @param strictMode
+     *            if <code>true</code> the client will strictly adhere to the
+     *            SRU standard and raise fatal errors on violations, if
+     *            <code>false</code> it will act more forgiving and ignore
+     *            certain violations
+     * @param parsers
+     *            a <code>Map</code> to store record schema to record data
+     *            parser mappings
+     */
+    SRUSimpleClient(SRUVersion defaultVersion,
+            Map<String, SRURecordDataParser> parsers) {
+        if (defaultVersion == null) {
+            throw new NullPointerException("version == null");
+        }
+        if (parsers == null) {
+            throw new NullPointerException("parsers == null");
+        }
+        this.defaultVersion = defaultVersion;
+        this.parsers        = parsers;
+        this.httpClient     = new DefaultHttpClient();
+        this.httpClient.getParams().setParameter(CoreProtocolPNames.USER_AGENT,
+                    "eu.clarin.sru.client/0.0.1");
+    }
+
+
+    /**
+     * Register a record data parser.
+     *
+     * @param parser
+     *            a parser instance
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if the supplied parser is invalid or a parser handing the
+     *             same record schema is already registered
+     */
+    public void registerRecordParser(SRURecordDataParser parser) {
+        if (parser == null) {
+            throw new NullPointerException("parser == null");
+        }
+        final String recordSchema = parser.getRecordSchema();
+        if (recordSchema == null) {
+            throw new NullPointerException("parser.getRecordSchema() == null");
+        }
+        if (recordSchema.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "parser.getRecordSchema() returns empty string");
+        }
+
+        if (!parsers.containsKey(recordSchema)) {
+            parsers.put(recordSchema, parser);
+        } else {
+            throw new IllegalArgumentException(
+                    "record data parser already registered: " + recordSchema);
+        }
+    }
+
+
+    /**
+     * Perform a <em>explain</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUExplainRequest} object
+     * @param handler
+     *            an instance of {@link SRUExplainHandler} to receive callbacks
+     *            when processing the result of this request
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @see SRUExplainRequest
+     * @see SRUExplainHandler
+     */
+    public void explain(SRUExplainRequest request, SRUExplainHandler handler)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (handler == null) {
+            throw new NullPointerException("handler == null");
+        }
+        logger.debug("performing explain request");
+
+        final long ts_start = System.nanoTime();
+
+        // create URI and perform request
+        final URI uri = request.makeURI(defaultVersion);
+        HttpResponse response = executeRequest(uri);
+        HttpEntity entity = response.getEntity();
+        if (entity == null) {
+            throw new SRUClientException("cannot get entity");
+        }
+
+        InputStream stream = null;
+        SRUXMLStreamReader reader = null;
+        try {
+            stream = entity.getContent();
+
+            final long ts_parsing = System.nanoTime();
+            reader = createReader(stream, true);
+            parseExplainResponse(reader, request, handler);
+            final long ts_end = System.nanoTime();
+
+            final long millisTotal =
+                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_start);
+            final long millisNetwork =
+                    TimeUnit.NANOSECONDS.toMillis(ts_parsing - ts_start);
+            final long millisProcessing =
+                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_parsing);
+            logger.debug("{} byte(s) in {} milli(s) " +
+                    "({} milli(s) network / {} milli(s) processing)",
+                    reader.getByteCount(), millisTotal, millisNetwork,
+                    millisProcessing);
+            handler.onRequestStatistics((int) reader.getByteCount(),
+                    millisTotal, millisNetwork, millisProcessing);
+        } catch (IllegalStateException e) {
+            throw new SRUClientException("error reading response", e);
+        } catch (IOException e) {
+            throw new SRUClientException("error reading response", e);
+        } catch (XMLStreamException e) {
+            throw new SRUClientException("error reading response", e);
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (XMLStreamException e) {
+                    /* IGNORE */
+                }
+            }
+            if (stream != null) {
+                try {
+                    stream.close();
+                } catch (IOException e) {
+                    /* IGNORE */
+                }
+            }
+
+            /* make sure to release allocated resources */
+            HttpClientUtils.closeQuietly(response);
+        }
+    }
+
+
+    /**
+     * Perform a <em>scan</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUScanRequest} object
+     * @param handler
+     *            an instance of {@link SRUScanHandler} to receive callbacks
+     *            when processing the result of this request
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @see SRUScanRequest
+     * @see SRUScanHandler
+     */
+    public void scan(SRUScanRequest request, SRUScanHandler handler)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (handler == null) {
+            throw new NullPointerException("handler == null");
+        }
+        logger.debug("performing scan request: scanClause = {}",
+                request.getScanClause());
+
+        final long ts_start = System.nanoTime();
+
+        // create URI and perform request
+        final URI uri = request.makeURI(defaultVersion);
+        HttpResponse response = executeRequest(uri);
+        HttpEntity entity = response.getEntity();
+        if (entity == null) {
+            throw new SRUClientException("cannot get entity");
+        }
+
+        InputStream stream = null;
+        SRUXMLStreamReader reader = null;
+        try {
+            stream = entity.getContent();
+
+            final long ts_parsing = System.nanoTime();
+            reader = createReader(stream, true);
+            parseScanResponse(reader, request, handler);
+            final long ts_end = System.nanoTime();
+
+            final long millisTotal =
+                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_start);
+            final long millisNetwork =
+                    TimeUnit.NANOSECONDS.toMillis(ts_parsing - ts_start);
+            final long millisProcessing =
+                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_parsing);
+            logger.debug("{} byte(s) in {} milli(s) " +
+                    "({} milli(s) network / {} milli(s) processing)",
+                    reader.getByteCount(), millisTotal, millisNetwork,
+                    millisProcessing);
+            handler.onRequestStatistics((int) reader.getByteCount(),
+                    millisTotal, millisNetwork, millisProcessing);
+        } catch (IllegalStateException e) {
+            throw new SRUClientException("error reading response", e);
+        } catch (IOException e) {
+            throw new SRUClientException("error reading response", e);
+        } catch (XMLStreamException e) {
+            throw new SRUClientException("error reading response", e);
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (XMLStreamException e) {
+                    /* IGNORE */
+                }
+            }
+            if (stream != null) {
+                try {
+                    stream.close();
+                } catch (IOException e) {
+                    /* IGNORE */
+                }
+            }
+
+            /* make sure to release allocated resources */
+            HttpClientUtils.closeQuietly(response);
+        }
+    }
+
+
+    /**
+     * Perform a <em>searchRetrieve</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUSearchRetrieveRequest} object
+     * @param handler
+     *            an instance of {@link SRUSearchRetrieveHandler} to receive
+     *            callbacks when processing the result of this request
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @see SRUSearchRetrieveRequest
+     * @see SRUSearchRetrieveHandler
+     */
+    public void searchRetrieve(SRUSearchRetrieveRequest request,
+            SRUSearchRetrieveHandler handler) throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (handler == null) {
+            throw new NullPointerException("handler == null");
+        }
+        logger.debug("performing searchRetrieve request: query = {}",
+                request.getQuery());
+
+        final long ts_start = System.nanoTime();
+
+        // create URI and perform request
+        final URI uri = request.makeURI(defaultVersion);
+        HttpResponse response = executeRequest(uri);
+        HttpEntity entity = response.getEntity();
+        if (entity == null) {
+            throw new SRUClientException("cannot get entity");
+        }
+
+        InputStream stream = null;
+        SRUXMLStreamReader reader = null;
+        try {
+            stream = entity.getContent();
+
+            final long ts_parsing = System.nanoTime();
+            reader = createReader(stream, true);
+            parseSearchRetrieveResponse(reader, request, handler);
+            final long ts_end = System.nanoTime();
+
+            final long millisTotal =
+                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_start);
+            final long millisNetwork =
+                    TimeUnit.NANOSECONDS.toMillis(ts_parsing - ts_start);
+            final long millisProcessing =
+                    TimeUnit.NANOSECONDS.toMillis(ts_end - ts_parsing);
+            logger.debug("{} byte(s) in {} milli(s) " +
+                    "({} milli(s) network / {} milli(s) processing)",
+                    reader.getByteCount(), millisTotal, millisNetwork,
+                    millisProcessing);
+            handler.onRequestStatistics((int) reader.getByteCount(),
+                    millisTotal, millisNetwork, millisProcessing);
+        } catch (IllegalStateException e) {
+            throw new SRUClientException("error reading response", e);
+        } catch (IOException e) {
+            throw new SRUClientException("error reading response", e);
+        } catch (XMLStreamException e) {
+            throw new SRUClientException("error reading response", e);
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (XMLStreamException e) {
+                    /* IGNORE */
+                }
+            }
+            if (stream != null) {
+                try {
+                    stream.close();
+                } catch (IOException e) {
+                    /* IGNORE */
+                }
+            }
+
+            /* make sure to release allocated resources */
+            HttpClientUtils.closeQuietly(response);
+        }
+    }
+
+
+    private HttpResponse executeRequest(URI uri) throws SRUClientException {
+        HttpGet request = null;
+        HttpResponse response = null;
+        try {
+            logger.debug("submitting HTTP request: {}", uri.toString());
+            try {
+                request = new HttpGet(uri);
+                response = httpClient.execute(request);
+                StatusLine status = response.getStatusLine();
+                if (status.getStatusCode() != HttpStatus.SC_OK) {
+                    if (status.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
+                        throw new SRUClientException("not found: " + uri);
+                    } else {
+                        throw new SRUClientException("unexpected status: " +
+                                status.getStatusCode());
+                    }
+                }
+                return response;
+            } catch (ClientProtocolException e) {
+                throw new SRUClientException("client protocol exception", e);
+            } catch (UnknownHostException e) {
+                throw new SRUClientException(
+                        "unknown host: " + uri.getHost(), e);
+            } catch (IOException e) {
+                String msg = null;
+                if ((e.getMessage() != null) && !e.getMessage().isEmpty()) {
+                    msg = e.getMessage();
+                }
+                throw new SRUClientException(msg != null
+                        ? msg
+                        : "input/output error", e);
+            }
+        } catch (SRUClientException e) {
+            /*
+             * if an error occurred, make sure we are freeing up the resources
+             * we've used
+             */
+            if (response != null) {
+                try {
+                    EntityUtils.consume(response.getEntity());
+                } catch (IOException ex) {
+                    /* IGNORE */
+                }
+
+                /* make sure to release allocated resources */
+                HttpClientUtils.closeQuietly(response);
+            }
+            if (request != null) {
+                request.abort();
+            }
+            throw e;
+        }
+    }
+
+
+    private void parseExplainResponse(final SRUXMLStreamReader reader,
+            final SRUExplainRequest request, final SRUExplainHandler handler)
+            throws SRUClientException {
+        logger.debug("parsing 'explain' response (mode = {})",
+                (request.isStrictMode() ? "strict" : "non-strict"));
+        /*
+         * Eventually, SRUClient should always parse explain record data.
+         * However, for now, make caller explicitly ask for it.
+         */
+        final boolean parse = request.isParseRecordDataEnabled();
+        if (!parse) {
+            logger.debug("parsing of explain record data skipped");
+        }
+        doParseExplainResponse(reader, request, handler, parse);
+    }
+
+
+    private void doParseExplainResponse(SRUXMLStreamReader reader,
+            SRUAbstractRequest request, SRUExplainHandler handler,
+            boolean parseRecordData) throws SRUClientException {
+        try {
+            final boolean strictMode = request.isStrictMode();
+
+            // explainResponse
+            reader.readStart(SRU_NS, "explainResponse", true);
+
+            // explainResponse/version
+            SRUVersion version = parseVersion(reader);
+            logger.debug("version = {}, requested = {}",
+                    version, request.getVersionPerformed());
+
+            // explainResponse/record
+            reader.readStart(SRU_NS, "record", true);
+            if (parseRecordData) {
+                handler.onStartRecords(-1, null, -1);
+
+                SRURecordPacking packing = null;
+                if (!strictMode && reader.peekStart(SRU_NS, "recordPacking")) {
+                    packing = parseRecordPacking(reader, false);
+                    if (packing != null) {
+                        logger.error("element <recordPacking> must apperear " +
+                                "after element <recordSchema> within " +
+                                "element <record>");
+                    }
+                }
+                String schema =
+                        reader.readContent(SRU_NS, "recordSchema", true);
+                if (packing == null) {
+                    packing = parseRecordPacking(reader, strictMode);
+                }
+                logger.debug("schema = {}, packing = {}", schema, packing);
+
+                // explainResponse/record/recordData
+                reader.readStart(SRU_NS, "recordData", true);
+                reader.consumeWhitespace();
+
+                SRURecordData recordData = null;
+                SRUXMLStreamReader recordReader = null;
+
+                if (packing == SRURecordPacking.STRING) {
+                    /*
+                     * read content into temporary buffer and then use a new XML
+                     * reader to parse record data
+                     */
+                    final String data = reader.readString(true);
+                    InputStream in = new ByteArrayInputStream(data.getBytes());
+                    // FIXME: namespace context?
+                    recordReader = createReader(in, false);
+                } else {
+                    recordReader = reader;
+                }
+
+                try {
+                    proxy.reset(recordReader);
+                    /*
+                     * Try to parse explain record data with explain record
+                     * parser. It will throw an exception, if it cannot handle
+                     * the data.
+                     */
+                    recordData = explainRecordParser.parse(proxy,
+                            version, strictMode, schema);
+                } catch (XMLStreamException e) {
+                    throw new SRUClientException(
+                            "error parsing explain record", e);
+                } finally {
+                    /*
+                     * make sure, we're deallocate the record reader in case of
+                     * string record packing
+                     */
+                    if (packing == SRURecordPacking.STRING) {
+                        recordReader.closeCompletly();
+                    }
+                }
+                if (recordData == null) {
+                    // FIXME: error message
+                    throw new SRUClientException(
+                            "error parsing explain record");
+                }
+                reader.consumeWhitespace();
+                reader.readEnd(SRU_NS, "recordData", true);
+
+                if (version == SRUVersion.VERSION_1_2) {
+                    reader.readContent(SRU_NS, "recordIdentifier", false);
+                }
+                reader.readContent(SRU_NS, "recordPosition", false, -1);
+
+                // notify handler
+                handler.onRecord(null, -1, recordData);
+
+                if (reader.readStart(SRU_NS, "extraRecordData", false)) {
+                    reader.consumeWhitespace();
+                    proxy.reset(reader);
+                    try {
+                        handler.onExtraRecordData(null, -1, proxy);
+                    } catch (XMLStreamException e) {
+                        throw new SRUClientException("handler "
+                                + "triggered error while parsing "
+                                + "'extraRecordData'", e);
+                    }
+                    reader.consumeWhitespace();
+                    reader.readEnd(SRU_NS, "extraRecordData", true);
+                }
+
+                handler.onFinishRecords(-1);
+
+                reader.readEnd(SRU_NS, "record");
+            } else {
+                /*
+                 * do not really parse record and skip everything
+                 * until <record> end tag
+                 */
+                reader.readEnd(SRU_NS, "record", true);
+            }
+
+            // explainResponse/echoedExplainRequest
+            if (reader.readStart(SRU_NS, "echoedExplainRequest", false)) {
+                reader.readEnd(SRU_NS, "echoedExplainRequest", true);
+            }
+
+            /*
+             * common error: echoedExplainRequest in default namespace
+             */
+            if (reader.readStart("", "echoedExplainRequest", false)) {
+                logger.error("Element 'echoedExplainRequest' must be in SRU " +
+                        "namespace, but endpoint put it into default namespace");
+                if (strictMode) {
+                    throw new SRUClientException("Element " +
+                            "'echoedExplainRequest' must be in SRU namespace,"+
+                            " but endpoint put it into default namespace");
+                }
+                reader.readEnd("", "echoedExplainRequest", true);
+            }
+
+            // explainResponse/diagnostics
+            final List<SRUDiagnostic> diagnostics =
+                    parseDiagnostics(reader, strictMode);
+            if (diagnostics != null) {
+                handler.onDiagnostics(diagnostics);
+            }
+
+            // explainResponse/extraResponseData
+            if (reader.readStart(SRU_NS, "extraResponseData", false)) {
+                reader.consumeWhitespace();
+                proxy.reset(reader);
+                try {
+                    handler.onExtraResponseData(proxy);
+                } catch (XMLStreamException e) {
+                    throw new SRUClientException("handler triggered "
+                            + "error while parsing 'extraResponseData'", e);
+                }
+                reader.consumeWhitespace();
+                reader.readEnd(SRU_NS, "extraResponseData", true);
+            }
+
+            reader.readEnd(SRU_NS, "explainResponse");
+        } catch (XMLStreamException e) {
+            throw new SRUClientException(e.getMessage(), e);
+        }
+    }
+
+
+    private void parseScanResponse(final SRUXMLStreamReader reader,
+            final SRUScanRequest request, final SRUScanHandler handler)
+            throws SRUClientException {
+        try {
+            /*
+             * if the endpoint cannot determine the operation, it should create
+             * a explain response.
+             */
+            if (reader.peekStart(SRU_NS, "explainResponse")) {
+                doParseExplainResponse(reader, request, new SRUExplainHandler() {
+                    @Override
+                    public void onRequestStatistics(int bytes, long millisTotal,
+                            long millisNetwork, long millisParsing) {
+                    }
+
+
+                    @Override
+                    public void onExtraResponseData(XMLStreamReader reader)
+                            throws XMLStreamException, SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+                            throws SRUClientException {
+                        handler.onDiagnostics(diagnostics);
+                    }
+
+
+                    @Override
+                    public void onStartRecords(int numberOfRecords,
+                            String resultSetId, int resultSetIdleTime)
+                            throws SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onFinishRecords(int nextRecordPosition)
+                            throws SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onRecord(String identifier, int position,
+                            SRURecordData data) throws SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onExtraRecordData(String identifier,
+                            int position, XMLStreamReader reader)
+                            throws XMLStreamException, SRUClientException {
+                    }
+                }, false);
+            } else {
+                final boolean strictMode = request.isStrictMode();
+
+                logger.debug("parsing 'scan' response (mode = {})",
+                        (strictMode ? "strict" : "non-strict"));
+
+
+                // scanResponse
+                reader.readStart(SRU_NS, "scanResponse", true);
+
+                // scanResponse/version
+                SRUVersion version = parseVersion(reader);
+                logger.debug("version = {}, requested = {}", version,
+                        request.getVersionPerformed());
+
+                // scanResponse/terms
+                if (reader.readStart(SRU_NS, "terms", false)) {
+                    boolean first = true;
+                    while (reader.readStart(SRU_NS, "term", first)) {
+                        if (first) {
+                            first = false;
+                            handler.onStartTerms();
+                        }
+
+                        // scanResponse/terms/value
+                        String value =
+                                reader.readContent(SRU_NS, "value", true);
+
+                        // scanResponse/terms/numberOfRecords
+                        int numberOfRecords = reader.readContent(SRU_NS,
+                                "numberOfRecords", false, -1);
+
+                        // scanResponse/terms/displayTerm
+                        String displayTerm = reader.readContent(SRU_NS,
+                                "displayTerm", false);
+
+                        // scanResponse/terms/whereInList
+                        String s = reader.readContent(SRU_NS,
+                                "whereInList", false);
+                        SRUWhereInList whereInList = null;
+                        if (s != null) {
+                            if ("first".equals(s)) {
+                                whereInList = SRUWhereInList.FIRST;
+                            } else if ("last".equals(s)) {
+                                whereInList = SRUWhereInList.LAST;
+                            } else if ("only".equals(s)) {
+                                whereInList = SRUWhereInList.ONLY;
+                            } else if ("inner".equals(s)) {
+                                whereInList = SRUWhereInList.INNER;
+                            } else {
+                                throw new SRUClientException(
+                                        "invalid value for 'whereInList': " + s);
+                            }
+                        }
+                        logger.debug("value = {}, numberOfRecords = {}, " +
+                                "displayTerm = {}, whereInList = {}", value,
+                                numberOfRecords, displayTerm, whereInList);
+                        handler.onTerm(value, numberOfRecords, displayTerm,
+                                whereInList);
+
+                        // scanResponse/terms/extraTermData
+                        if (reader.readStart(SRU_NS, "extraTermData", first)) {
+                            reader.consumeWhitespace();
+                            proxy.reset(reader);
+                            try {
+                                handler.onExtraTermData(value, proxy);
+                            } catch (XMLStreamException e) {
+                                throw new SRUClientException("handler "
+                                        + "triggered error while parsing "
+                                        + "'extraTermData'", e);
+                            }
+                            reader.consumeWhitespace();
+                            reader.readEnd(SRU_NS, "extraTermData", true);
+                        }
+                        reader.readEnd(SRU_NS, "term", true);
+
+                    } // while
+                    reader.readEnd(SRU_NS, "terms");
+                    handler.onFinishTerms();
+                }
+
+                // scanResponse/echoedScanRequest
+                if (reader.readStart(SRU_NS, "echoedScanRequest", false)) {
+                    reader.readEnd(SRU_NS, "echoedScanRequest", true);
+                }
+
+                /*
+                 * common error: echoedScanRequest in default namespace
+                 */
+                if (reader.readStart("", "echoedScanRequest", false)) {
+                    logger.error("Element 'echoedScanRequest' must be in SRU namespace, but endpoint put it into default namespace");
+                    if (strictMode) {
+                        throw new SRUClientException("Element 'echoedScanRequest' must be in SRU namespace, but endpoint put it into default namespace");
+                    }
+                    reader.readEnd("", "echoedScanRequest", true);
+                }
+
+                // scanResponse/diagnostics
+                final List<SRUDiagnostic> diagnostics =
+                        parseDiagnostics(reader, strictMode);
+                if (diagnostics != null) {
+                    handler.onDiagnostics(diagnostics);
+                }
+
+                // scanResponse/extraResponseData
+                if (reader.readStart(SRU_NS, "extraResponseData", false)) {
+                    reader.consumeWhitespace();
+                    proxy.reset(reader);
+                    try {
+                        handler.onExtraResponseData(proxy);
+                    } catch (XMLStreamException e) {
+                        throw new SRUClientException("handler triggered "
+                                + "error while parsing 'extraResponseData'", e);
+                    }
+                    reader.consumeWhitespace();
+                    reader.readEnd(SRU_NS, "extraResponseData", true);
+                }
+
+                reader.readEnd(SRU_NS, "scanResponse");
+            }
+        } catch (XMLStreamException e) {
+            throw new SRUClientException(e.getMessage(), e);
+        }
+    }
+
+
+    private void parseSearchRetrieveResponse(final SRUXMLStreamReader reader,
+            final SRUSearchRetrieveRequest request,
+            final SRUSearchRetrieveHandler handler) throws SRUClientException {
+        try {
+            /*
+             * if the endpoint cannot determine the operation, it should create
+             * a explain response.
+             */
+            if (reader.peekStart(SRU_NS, "explainResponse")) {
+                doParseExplainResponse(reader, request, new SRUExplainHandler() {
+                    @Override
+                    public void onRequestStatistics(int bytes, long millisTotal,
+                            long millisNetwork, long millisParsing) {
+                    }
+
+
+                    @Override
+                    public void onExtraResponseData(XMLStreamReader reader)
+                            throws XMLStreamException, SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+                            throws SRUClientException {
+                        handler.onDiagnostics(diagnostics);
+                    }
+
+
+                    @Override
+                    public void onStartRecords(int numberOfRecords,
+                            String resultSetId, int resultSetIdleTime)
+                            throws SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onFinishRecords(int nextRecordPosition)
+                            throws SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onRecord(String identifier, int position,
+                            SRURecordData data) throws SRUClientException {
+                    }
+
+
+                    @Override
+                    public void onExtraRecordData(String identifier,
+                            int position, XMLStreamReader reader)
+                            throws XMLStreamException, SRUClientException {
+                    }
+                }, false);
+            } else {
+                final boolean strictMode = request.isStrictMode();
+
+                logger.debug("parsing 'searchRetrieve' response (mode = {})",
+                        (strictMode ? "strict" : "non-strict"));
+
+                // searchRetrieveResponse
+                reader.readStart(SRU_NS, "searchRetrieveResponse", true);
+
+                // searchRetrieveResponse/version
+                SRUVersion version = parseVersion(reader);
+                logger.debug("version = {}, requested = {}", version,
+                        request.getVersionPerformed());
+
+                // searchRetrieveResponse/numberOfRecords
+                int numberOfRecords = reader.readContent(SRU_NS,
+                        "numberOfRecords", true, -1);
+
+                // searchRetrieveResponse/resultSetId
+                String resultSetId = reader.readContent(SRU_NS,
+                        "resultSetId", false);
+
+                // searchRetrieveResponse/resultSetIdleTime
+                int resultSetIdleTime = reader.readContent(SRU_NS,
+                        "resultSetIdleTime", false, -1);
+
+                logger.debug("numberOfRecords = {}, resultSetId = {}, " +
+                        "resultSetIdleTime = {}", numberOfRecords,
+                        resultSetId, resultSetIdleTime);
+
+                // searchRetrieveResponse/results
+                if (numberOfRecords > 0) {
+                    /*
+                     * some endpoints set numberOfRecords but do not serialize
+                     * any records, e.g if requested record schema is not
+                     * supported
+                     */
+                    int recordCount = 0;
+                    if (reader.readStart(SRU_NS, "records", false)) {
+                        // searchRetrieveResponse/records/record
+                        boolean first = true;
+                        while (reader.readStart(SRU_NS, "record", first)) {
+                            if (first) {
+                                first = false;
+                                handler.onStartRecords(numberOfRecords,
+                                        resultSetId, resultSetIdleTime);
+                            }
+
+                            /*
+                             * common error: recordPacking before recordSchema
+                             */
+                            SRURecordPacking packing = null;
+                            if (!strictMode &&
+                                    reader.peekStart(SRU_NS, "recordPacking")) {
+                                packing = parseRecordPacking(reader, false);
+                                if (packing != null) {
+                                    logger.error("element <recordPacking> " +
+                                            "must appear after element " +
+                                            "<recordSchema> within " +
+                                            "element <record>");
+                                }
+                            }
+                            String schema = reader.readContent(SRU_NS,
+                                    "recordSchema", true);
+                            if (packing == null) {
+                                packing = parseRecordPacking(reader, strictMode);
+                            }
+
+                            logger.debug("schema = {}, packing = {}, " +
+                                    "requested packing = {}",
+                                    schema, packing,
+                                    request.getRecordPacking());
+
+                            if ((request.getRecordPacking() != null) &&
+                                    (packing != request.getRecordPacking())) {
+                                final SRURecordPacking p =
+                                        request.getRecordPacking();
+                                logger.error("requested '{}' record packing, " +
+                                        "but server responded with '{}' " +
+                                        "record packing",
+                                        p.getStringValue(),
+                                        packing.getStringValue());
+                                if (strictMode) {
+                                    throw new SRUClientException("requested '" +
+                                            p.getStringValue() +
+                                            "' record packing, but server " +
+                                            "responded with '" +
+                                            packing.getStringValue() +
+                                            "' record packing");
+                                }
+                            }
+
+                            // searchRetrieveResponse/record/recordData
+                            reader.readStart(SRU_NS, "recordData", true);
+                            reader.consumeWhitespace();
+
+                            SRURecordData recordData = null;
+                            SRUDiagnostic surrogate = null;
+                            SRUXMLStreamReader recordReader = null;
+
+                            if (packing == SRURecordPacking.STRING) {
+                                /*
+                                 * read content into temporary buffer and then
+                                 * use a new XML reader to parse record data
+                                 */
+                                final String data = reader.readString(true);
+                                InputStream in = new ByteArrayInputStream(
+                                        data.getBytes());
+                                // FIXME: namespace context?
+                                recordReader = createReader(in, false);
+                            } else {
+                                recordReader = reader;
+                            }
+
+                            if (SRU_DIAGNOSTIC_RECORD_SCHEMA.equals(schema)) {
+                                surrogate = parseDiagnostic(recordReader, true,
+                                        strictMode);
+                            } else {
+                                SRURecordDataParser parser = findParser(schema);
+                                if (parser != null) {
+                                    try {
+                                        proxy.reset(recordReader);
+                                        recordData = parser.parse(proxy);
+                                    } catch (XMLStreamException e) {
+                                        throw new SRUClientException(
+                                                "error parsing record", e);
+                                    } finally {
+                                        /*
+                                         * make sure, we deallocate the record
+                                         * reader in case of string record
+                                         * packing
+                                         */
+                                        if (packing == SRURecordPacking.STRING) {
+                                            recordReader.closeCompletly();
+                                        }
+                                    }
+                                    if (recordData == null) {
+                                        logger.debug("record parser did not parse "
+                                                + "record correctly and returned "
+                                                + "null; injecting client side "
+                                                + "surrogate diagnostic");
+                                        surrogate = new SRUDiagnostic(
+                                                SRUClientDiagnostics.DIAG_RECORD_PARSER_NULL,
+                                                null,
+                                                "Record parser for schema '" +
+                                                        schema +
+                                                        "' did not " +
+                                                        "parse record correctly " +
+                                                        "and errornously " +
+                                                        "returned null.");
+                                    }
+                                } else {
+                                    /*
+                                     * no record parser found, inject a
+                                     * surrogate diagnostic
+                                     */
+                                    logger.debug(
+                                            "no record data parser found "
+                                                    + "for schema '{}'; injecting client "
+                                                    + "side surrogate diagnostic",
+                                            schema);
+                                    surrogate = new SRUDiagnostic(
+                                            SRUClientDiagnostics.DIAG_NO_RECORD_PARSER,
+                                            schema,
+                                            "No record data parser for schema '" +
+                                                    schema + "' found.");
+                                }
+                            }
+
+                            reader.consumeWhitespace();
+                            reader.readEnd(SRU_NS, "recordData", true);
+
+                            String identifier = null;
+                            if (version == SRUVersion.VERSION_1_2) {
+                                identifier = reader.readContent(SRU_NS,
+                                        "recordIdentifier", false);
+                            }
+
+                            int position = reader.readContent(SRU_NS,
+                                    "recordPosition", false, -1);
+
+                            logger.debug("recordIdentifier = {}, " +
+                                    "recordPosition = {}",
+                                    identifier, position);
+
+                            // notify handler
+                            if (surrogate != null) {
+                                handler.onSurrogateRecord(identifier,
+                                        position, surrogate);
+                            } else {
+                                if (recordData != null) {
+                                    handler.onRecord(identifier,
+                                            position, recordData);
+                                }
+                            }
+
+                            if (reader.readStart(SRU_NS,
+                                    "extraRecordData", false)) {
+                                reader.consumeWhitespace();
+                                proxy.reset(reader);
+                                try {
+                                    handler.onExtraRecordData(identifier,
+                                            position, proxy);
+                                } catch (XMLStreamException e) {
+                                    throw new SRUClientException("handler " +
+                                            "triggered error while parsing " +
+                                            "'extraRecordData'", e);
+                                }
+                                reader.consumeWhitespace();
+                                reader.readEnd(SRU_NS, "extraRecordData", true);
+                            }
+
+                            reader.readEnd(SRU_NS, "record");
+                            recordCount++;
+                        } // while
+                        reader.readEnd(SRU_NS, "records");
+                    }
+                    if (recordCount == 0) {
+                        logger.error("endpoint declared {} results, but response contained no <record> elements (behavior may violate SRU specification)", numberOfRecords);
+                    } else if ((request.getMaximumRecords() != -1) && (recordCount > request.getMaximumRecords())) {
+                        logger.error("endpoint did not honour 'maximumRecords' request parameter and responded with {} records instead of a maximum of {}", recordCount, request.getMaximumRecords());
+                    }
+                } else {
+                    /*
+                     * provide a better error format, if endpoints responds with
+                     * an empty <records> element
+                     */
+                    if (reader.readStart(SRU_NS, "records", false)) {
+                        int bad = 0;
+                        while (reader.readStart(SRU_NS, "record", false)) {
+                            bad++;
+                            reader.readEnd(SRU_NS, "record", true);
+                        }
+                        reader.readEnd(SRU_NS, "records", true);
+                        if (bad == 0) {
+                            logger.error("endpoint declared 0 results, but " +
+                                    "response contained an empty 'records' " +
+                                    "element");
+                            if (strictMode) {
+                                throw new SRUClientException(
+                                        "endpoint declared 0 results, but " +
+                                        "response contained an empty " +
+                                        "'records' element (behavior " +
+                                        "violates SRU specification)");
+                            }
+                        } else {
+                            logger.error("endpoint declared 0 results, but " +
+                                    "response contained " + bad +
+                                    " record(s)");
+                            if (strictMode) {
+                                throw new SRUClientException(
+                                            "endpoint declared 0 results, " +
+                                            "but response containted " + bad +
+                                            " records (behavior may violate " +
+                                            "SRU specification)");
+                            }
+                        }
+                    }
+                }
+
+                int nextRecordPosition = reader.readContent(SRU_NS,
+                        "nextRecordPosition", false, -1);
+                logger.debug("nextRecordPosition = {}", nextRecordPosition);
+                handler.onFinishRecords(nextRecordPosition);
+
+                // searchRetrieveResponse/echoedSearchRetrieveResponse
+                if (reader.readStart(SRU_NS,
+                        "echoedSearchRetrieveRequest", false)) {
+                    reader.readEnd(SRU_NS, "echoedSearchRetrieveRequest", true);
+                }
+
+                /*
+                 * common error: echoedSearchRetrieveRequest in
+                 * default namespace
+                 */
+                if (reader.readStart("", "echoedSearchRetrieveRequest", false)) {
+                    logger.error("Element 'echoedSearchRetrieveRequest' " +
+                            "must be in SRU namespace, but endpoint put it " +
+                            "into default namespace");
+                    if (strictMode) {
+                        throw new SRUClientException(
+                                "Element 'echoedSearchRetrieveRequest' must " +
+                                "be in SRU namespace, but endpoint put it " +
+                                "into default namespace");
+                    }
+                    reader.readEnd("", "echoedSearchRetrieveRequest", true);
+                }
+
+                // searchRetrieveResponse/diagnostics
+                final List<SRUDiagnostic> diagnostics =
+                        parseDiagnostics(reader, strictMode);
+                if (diagnostics != null) {
+                    handler.onDiagnostics(diagnostics);
+                }
+
+                // explainResponse/extraResponseData
+                if (reader.readStart(SRU_NS, "extraResponseData", false)) {
+                    reader.consumeWhitespace();
+                    proxy.reset(reader);
+                    try {
+                        handler.onExtraResponseData(proxy);
+                    } catch (XMLStreamException e) {
+                        throw new SRUClientException("handler triggered "
+                                + "error while parsing 'extraResponseData'", e);
+                    }
+                    reader.consumeWhitespace();
+                    reader.readEnd(SRU_NS, "extraResponseData", true);
+                }
+
+                reader.readEnd(SRU_NS, "searchRetrieveResponse");
+            }
+        } catch (XMLStreamException e) {
+            throw new SRUClientException(e.getMessage(), e);
+        }
+    }
+
+
+    private static SRUVersion parseVersion(SRUXMLStreamReader reader)
+        throws XMLStreamException, SRUClientException {
+        final String v = reader.readContent(SRU_NS, "version", true);
+        if (VERSION_1_1.equals(v)) {
+            return SRUVersion.VERSION_1_1;
+        } else if (VERSION_1_2.equals(v)) {
+            return SRUVersion.VERSION_1_2;
+        } else {
+            throw new SRUClientException("invalid value '" + v +
+                    "' for version (valid values are: '" + VERSION_1_1 +
+                    "' and '" + VERSION_1_2 + "')");
+        }
+    }
+
+
+    private static List<SRUDiagnostic> parseDiagnostics(
+            SRUXMLStreamReader reader, boolean strictMode)
+            throws XMLStreamException, SRUClientException {
+        if (reader.readStart(SRU_NS, "diagnostics", false)) {
+            List<SRUDiagnostic> diagnostics = null;
+
+            SRUDiagnostic diagnostic = null;
+            while ((diagnostic = parseDiagnostic(reader,
+                    (diagnostics == null), strictMode)) != null) {
+                if (diagnostics == null) {
+                    diagnostics = new ArrayList<SRUDiagnostic>();
+                }
+                diagnostics.add(diagnostic);
+            } // while
+            reader.readEnd(SRU_NS, "diagnostics");
+            return diagnostics;
+        } else {
+            return null;
+        }
+    }
+
+
+    private static SRUDiagnostic parseDiagnostic(SRUXMLStreamReader reader,
+            boolean required, boolean strictMode) throws XMLStreamException,
+            SRUClientException {
+        if (reader.readStart(SRU_DIAGNOSIC_NS, "diagnostic", required)) {
+
+            // diagnostic/uri
+            String uri = reader.readContent(SRU_DIAGNOSIC_NS, "uri", true);
+
+            String details = null;
+            String message = null;
+            if (strictMode) {
+                // diagnostic/details
+                details = reader.readContent(SRU_DIAGNOSIC_NS, "details",
+                        false, true);
+
+                // diagnostic/message
+                message = reader.readContent(SRU_DIAGNOSIC_NS, "message",
+                        false, true);
+            } else {
+                /*
+                 * common error: diagnostic/details and diagnostic/message may
+                 * appear in any order
+                 */
+                if (reader.peekStart(SRU_DIAGNOSIC_NS, "details")) {
+                    details = reader.readContent(SRU_DIAGNOSIC_NS, "details",
+                            false, false);
+                    message = reader.readContent(SRU_DIAGNOSIC_NS, "message",
+                            false, false);
+                } else {
+                    message = reader.readContent(SRU_DIAGNOSIC_NS, "message",
+                            false, false);
+                    details = reader.readContent(SRU_DIAGNOSIC_NS, "details",
+                            false, false);
+                    if ((message != null) && (details != null)) {
+                        logger.error("element <message> and element " +
+                                "<details> within element <diagnostic> " +
+                                "appeared in wrong order");
+                    }
+                }
+            }
+
+            if ((details != null) && details.isEmpty()) {
+                details = null;
+                logger.debug("omitting empty element <details> " +
+                        "within element <diagnostic>");
+            }
+            if ((message != null) && message.isEmpty()) {
+                message = null;
+                logger.debug("omitting empty element <message> " +
+                        "within element <diagnostic>");
+            }
+
+            reader.readEnd(SRU_DIAGNOSIC_NS, "diagnostic");
+
+            logger.debug("diagnostic: uri={}, detail={}, message={}",
+                    uri, details, message);
+            return new SRUDiagnostic(uri, details, message);
+        } else {
+            return null;
+        }
+    }
+
+
+    private static SRURecordPacking parseRecordPacking(
+            SRUXMLStreamReader reader, boolean strictMode)
+            throws XMLStreamException, SRUClientException {
+        final String v = reader.readContent(SRU_NS, "recordPacking", true);
+
+        if (RECORD_PACKING_XML.equals(v)) {
+            return SRURecordPacking.XML;
+        } else if (RECORD_PACKING_STRING.equals(v)) {
+            return SRURecordPacking.STRING;
+        } else if (!strictMode && RECORD_PACKING_XML.equalsIgnoreCase(v)) {
+            logger.error("invalid value '{}' for record packing, should be '{}'",
+                         v, RECORD_PACKING_XML);
+            return SRURecordPacking.XML;
+        } else if (!strictMode && RECORD_PACKING_STRING.equalsIgnoreCase(v)) {
+            logger.error("invalid value '{}' for record packing, should be '{}'",
+                         v, RECORD_PACKING_STRING);
+            return SRURecordPacking.STRING;
+
+        } else {
+            throw new SRUClientException("invalid value '" + v +
+                    "' for record packing (valid values are: '" +
+                    RECORD_PACKING_XML + "' and '" + RECORD_PACKING_STRING +
+                    "')");
+        }
+    }
+
+
+    private SRURecordDataParser findParser(String schema) {
+        SRURecordDataParser parser = parsers.get(schema);
+        if (parser == null) {
+            parser = parsers.get(SRUClientConstants.RECORD_DATA_PARSER_SCHEMA_ANY);
+        }
+        return parser;
+    }
+
+
+    private SRUXMLStreamReader createReader(InputStream in, boolean wrap)
+            throws XMLStreamException {
+        return new SRUXMLStreamReader(in, wrap);
+    }
+
+} // class SRUSimpleClient
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSurrogateRecordData.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSurrogateRecordData.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUSurrogateRecordData.java	(revision 2998)
@@ -0,0 +1,90 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+
+
+/**
+ * A record data implementation to hold surrogate diagnostics.
+ */
+public final class SRUSurrogateRecordData implements SRURecordData {
+    /**
+     * The record schema for SRU surrogate records.
+     */
+    public static final String RECORD_SCHEMA =
+            "info:srw/schema/1/diagnostic-v1.1";
+    private final SRUDiagnostic diagnostic;
+
+
+    SRUSurrogateRecordData(SRUDiagnostic diagnostic) {
+        this.diagnostic = diagnostic;
+    }
+
+
+    @Override
+    public String getRecordSchema() {
+        return RECORD_SCHEMA;
+    }
+
+
+    @Override
+    public boolean isTransient() {
+        return false;
+    }
+
+
+    /**
+     * Get the surrogate diagnostic.
+     *
+     * @return the surrogate diagnostic
+     */
+    public SRUDiagnostic getDiagnostic() {
+        return diagnostic;
+    }
+
+
+    /**
+     * Convenience method to get diagnostic's identifying URI.
+     *
+     * @return diagnostic code
+     */
+    public String getURI() {
+        return diagnostic.getURI();
+    }
+
+
+    /**
+     * Convenience method to get supplementary information for this diagnostic.
+     * The format for this value is often specified by the diagnostic code.
+     *
+     * @return supplementary information
+     */
+    public String getDetails() {
+        return diagnostic.getDetails();
+    }
+
+
+    /**
+     * Convenience method to get human readable message.
+     *
+     * @return human readable message
+     */
+    public String getMessage() {
+        return diagnostic.getMessage();
+    }
+
+} // class SRUSurrogateRecordData
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUTerm.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUTerm.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUTerm.java	(revision 2998)
@@ -0,0 +1,126 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import org.w3c.dom.DocumentFragment;
+
+
+/**
+ * Class to hold a single entry from a scan response.
+ *
+ * @see SRUScanResponse
+ */
+public final class SRUTerm {
+    private final String value;
+    private final int numberOfRecords;
+    private final String displayTerm;
+    private final SRUWhereInList whereInList;
+    private DocumentFragment extraTermData = null;
+
+
+    /**
+     * Constructor.
+     *
+     * @param value
+     *            value of the term
+     * @param numberOfRecords
+     *            number of record or <code>-1</code>
+     * @param displayTerm
+     *            a display string or <code>null</code>
+     * @param whereInList
+     *            flag or <code>null</code>
+     */
+    SRUTerm(String value, int numberOfRecords, String displayTerm,
+            SRUWhereInList whereInList) {
+        if (value == null) {
+            throw new NullPointerException("value == null");
+        }
+        this.value = value;
+        this.numberOfRecords = numberOfRecords;
+        this.displayTerm = displayTerm;
+        this.whereInList = whereInList;
+    }
+
+
+    /**
+     * Get the term as it appeared in the index.
+     *
+     * @return the value of the term
+     */
+    public String getValue() {
+        return value;
+    }
+
+
+    /**
+     * Get the number of records which would me matched by the term.
+     *
+     * @return the number of record or <code>-1</code> if unknown
+     */
+    public int getNumberOfRecords() {
+        return numberOfRecords;
+    }
+
+
+    /**
+     * Get A string to display to the end user in place of the term itself.
+     *
+     * @return a display string or <code>null</code> if unknown
+     */
+    public String getDisplayTerm() {
+        return displayTerm;
+    }
+
+
+    /**
+     * Get the flag to indicate the position of the term within the complete
+     * term list.
+     *
+     * @return the flag or <code>null</code> if unknown
+     */
+    public SRUWhereInList getWhereInList() {
+        return whereInList;
+    }
+
+
+    /**
+     * Get extra term data for this term.
+     *
+     * @return get an instance of {@link DocumentFragment} containing the XML
+     *         fragment for the extra term data from the SRU response or
+     *         <code>null</code> if none are available
+     */
+    public DocumentFragment getExtraTermData() {
+        return extraTermData;
+    }
+
+
+    /**
+     * Check, if this term has extra term data attached to it.
+     *
+     * @return <code>true</code> if extra term data is attached,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasExtraTermData() {
+        return extraTermData != null;
+    }
+
+    void setExtraTermData(DocumentFragment extraTermData) {
+        this.extraTermData = extraTermData;
+    }
+
+} // class SRUTerm
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUThreadedClient.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUThreadedClient.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUThreadedClient.java	(revision 2998)
@@ -0,0 +1,445 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * A client to perform SRU operations in parallel. The response of a SRU request
+ * is wrapped in a SRU response.
+ * <p>
+ * This client is reusable and thread-safe: the application may reuse a
+ * client object and may shared it between multiple threads. <br />
+ * NB: The registered {@link SRURecordDataParser} need to be thread-safe
+ * </p>
+ */
+public class SRUThreadedClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(SRUThreadedClient.class);
+    private final ConcurrentMap<String, SRURecordDataParser> parsers =
+            new ConcurrentHashMap<String, SRURecordDataParser>();
+    private final DocumentBuilderFactory documentBuilderFactory =
+            DocumentBuilderFactory.newInstance();
+    private final ThreadLocal<SRUClient> client;
+    private final ExecutorService executor;
+
+
+    /**
+     * Constructor. This constructor will create a <em>strict</em> client and
+     * use the default SRU version.
+     *
+     * @see SRUSimpleClient#DEFAULT_SRU_VERSION
+     */
+    public SRUThreadedClient() {
+        this(SRUSimpleClient.DEFAULT_SRU_VERSION);
+    }
+
+
+    /**
+     * Constructor.
+     *
+     * @param defaultVersion
+     *            the default version to use for SRU requests; may be overridden
+     *            by individual requests
+     */
+    public SRUThreadedClient(final SRUVersion defaultVersion) {
+        client = new ThreadLocal<SRUClient>() {
+            @Override
+            protected SRUClient initialValue() {
+                logger.debug("instantiated new sru client");
+                return new SRUClient(defaultVersion, parsers,
+                        documentBuilderFactory);
+            }
+        };
+        // TODO: make worker count configurable
+        int workerCount = Runtime.getRuntime().availableProcessors() * 2;
+        logger.debug("using {} workers", workerCount);
+        executor = Executors.newFixedThreadPool(workerCount, new Factory());
+    }
+
+
+    /**
+     * Register a record data parser.
+     *
+     * @param parser
+     *            a parser instance
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if the supplied parser is invalid or a parser handing the
+     *             same record schema is already registered
+     */
+    public void registerRecordParser(SRURecordDataParser parser) {
+        if (parser == null) {
+            throw new NullPointerException("parser == null");
+        }
+        final String recordSchema = parser.getRecordSchema();
+        if (recordSchema == null) {
+            throw new NullPointerException("parser.getRecordSchema() == null");
+        }
+        if (recordSchema.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "parser.getRecordSchema() returns empty string");
+        }
+
+        if (parsers.putIfAbsent(recordSchema, parser) != null) {
+            throw new IllegalArgumentException(
+                    "record data parser already registered: " + recordSchema);
+
+        }
+    }
+
+
+    /**
+     * Perform a <em>explain</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUExplainRequest} object
+     * @return a {@link Future} object that wraps a {@link SRUExplainResponse}
+     *         object
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @deprecated Use asynchronous callback interface
+     *             {@link #explain(SRUExplainRequest, SRUCallback)}. This method
+     *             will be removed in the future.
+     */
+    @Deprecated
+    public Future<SRUExplainResponse> explain(SRUExplainRequest request)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (executor.isShutdown()) {
+            throw new SRUClientException("client is shutting down");
+        }
+        return executor.submit(new Request<SRUExplainRequest, SRUExplainResponse>(request) {
+            @Override
+            public SRUExplainResponse doRequest(SRUClient client)
+                    throws SRUClientException {
+                return client.explain(request);
+            }
+        });
+    }
+
+
+    /**
+     * Perform a <em>explain</em> operation and invoke a user supplied callback
+     * after the request has been completed.
+     *
+     * @param request
+     *            an instance of a {@link SRUExplainRequest} object
+     * @param callback
+     *            the callback to be invoked
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @see SRUCallback
+     */
+    public void explain(final SRUExplainRequest request,
+            final SRUCallback<SRUExplainRequest, SRUExplainResponse> callback)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (callback == null) {
+            throw new NullPointerException("callback == null");
+        }
+        if (executor.isShutdown()) {
+            throw new SRUClientException("client is shutting down");
+        }
+        executor.submit(new AsyncRequest<SRUExplainRequest, SRUExplainResponse>(
+                request, callback) {
+            @Override
+            protected SRUExplainResponse doRequest(SRUClient client)
+                    throws SRUClientException {
+                return client.explain(request);
+            }
+        });
+    }
+
+
+    /**
+     * Perform a <em>scan</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUScanRequest} object
+     * @return a {@link Future} object that wraps a {@link SRUScanResponse}
+     *         object
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @deprecated Use asynchronous callback interface
+     *             {@link #scan(SRUScanRequest, SRUCallback)}. This method
+     *             will be removed in the future.
+     */
+    @Deprecated
+    public Future<SRUScanResponse> scan(SRUScanRequest request)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (executor.isShutdown()) {
+            throw new SRUClientException("client is shutting down");
+        }
+        return executor.submit(new Request<SRUScanRequest, SRUScanResponse>(
+                request) {
+            @Override
+            public SRUScanResponse doRequest(SRUClient client)
+                    throws SRUClientException {
+                return client.scan(request);
+            }
+        });
+    }
+
+
+    /**
+     * Perform a <em>scan</em> operation and invoke a user supplied callback
+     * after the request has been completed.
+     *
+     * @param request
+     *            an instance of a {@link SRUScanRequest} object
+     * @param callback
+     *            the callback to be invoked
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @see SRUCallback
+     */
+    public void scan(final SRUScanRequest request,
+            final SRUCallback<SRUScanRequest, SRUScanResponse> callback)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (callback == null) {
+            throw new NullPointerException("callback == null");
+        }
+        if (executor.isShutdown()) {
+            throw new SRUClientException("client is shutting down");
+        }
+        executor.submit(new AsyncRequest<SRUScanRequest, SRUScanResponse>(
+                request, callback) {
+            @Override
+            protected SRUScanResponse doRequest(SRUClient client)
+                    throws SRUClientException {
+                return client.scan(request);
+            }
+        });
+    }
+
+
+    /**
+     * Perform a <em>searchRetrieve</em> operation.
+     *
+     * @param request
+     *            an instance of a {@link SRUSearchRetrieveRequest} object
+     * @return a {@link Future} object that wraps a {@link SRUExplainResponse}
+     *         object
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @deprecated Use asynchronous callback interface
+     *             {@link #searchRetrieve(SRUSearchRetrieveRequest, SRUCallback)}
+     *             . This method will be removed in the future.
+     */
+    @Deprecated
+    public Future<SRUSearchRetrieveResponse> searchRetrieve(
+            SRUSearchRetrieveRequest request) throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (executor.isShutdown()) {
+            throw new SRUClientException("client is shutting down");
+        }
+        return executor.submit(new Request<SRUSearchRetrieveRequest,
+                SRUSearchRetrieveResponse>(request) {
+            @Override
+            protected SRUSearchRetrieveResponse doRequest(SRUClient client)
+                    throws SRUClientException {
+                return client.searchRetrieve(request);
+            }
+        });
+    }
+
+
+    /**
+     * Perform a <em>searchRetrieve</em> operation and invoke a user supplied
+     * callback after the request has been completed.
+     *
+     * @param request
+     *            an instance of a {@link SRUSearchRetrieveRequest} object
+     * @param callback
+     *            the callback to be invoked
+     * @throws SRUClientException
+     *             if an unrecoverable error occurred
+     * @throws NullPointerException
+     *             if any required argument is <code>null</code>
+     * @see SRUCallback
+     */
+    public void searchRetrieve(final SRUSearchRetrieveRequest request,
+            final SRUCallback<SRUSearchRetrieveRequest, SRUSearchRetrieveResponse> callback)
+            throws SRUClientException {
+        if (request == null) {
+            throw new NullPointerException("request == null");
+        }
+        if (callback == null) {
+            throw new NullPointerException("callback == null");
+        }
+        if (executor.isShutdown()) {
+            throw new SRUClientException("client is shutting down");
+        }
+        executor.submit(new AsyncRequest<SRUSearchRetrieveRequest,
+                SRUSearchRetrieveResponse>(request, callback) {
+            @Override
+            protected SRUSearchRetrieveResponse doRequest(SRUClient client)
+                    throws SRUClientException {
+                return client.searchRetrieve(request);
+            }
+        });
+    }
+
+
+    /**
+     * Invokes <code>shutdown</code> when this no longer referenced
+     */
+    @Override
+    protected void finalize() throws Throwable {
+        shutdown();
+    }
+
+
+    /**
+     * Initiates an orderly shutdown in which previously submitted requests are
+     * executed, but no new requests will be accepted.
+     */
+    public void shutdown() {
+        executor.shutdown();
+    }
+
+
+    /**
+     * Terminate the client but drain queued requests.
+     */
+    public void shutdownNow() {
+        executor.shutdownNow();
+    }
+
+
+    private class Factory implements ThreadFactory {
+        @Override
+        public Thread newThread(Runnable r) {
+            return new Thread(r) {
+                @Override
+                public void run() {
+                    try {
+                        logger.debug("launched new worker");
+
+                        // pre-initialize client
+                        client.get();
+
+                        // do work
+                        super.run();
+                    } finally {
+                        // do not leak resources and clean ThreadLocal ...
+                        client.remove();
+                        logger.debug("cleared sru client");
+                    }
+                }
+            };
+        }
+    }
+
+
+    private abstract class Request<V extends SRUAbstractRequest, S extends SRUAbstractResponse<V>>
+            implements Callable<S> {
+        protected final V request;
+        private long now = System.nanoTime();
+
+
+        Request(V request) {
+            this.request = request;
+        }
+
+
+        @Override
+        public final S call() throws Exception {
+            final SRUClient c = client.get();
+            c.setTimeQueued(System.nanoTime() - now);
+            return doRequest(c);
+        }
+
+
+        protected abstract S doRequest(SRUClient client)
+                throws SRUClientException;
+    }
+
+
+    private abstract class AsyncRequest<V extends SRUAbstractRequest,
+                                        S extends SRUAbstractResponse<V>>
+            implements Runnable {
+        protected final V request;
+        private long now = System.nanoTime();
+        private final SRUCallback<V, S> callback;
+
+
+        public AsyncRequest(V request, SRUCallback<V, S> callback) {
+            this.callback = callback;
+            this.request = request;
+        }
+
+
+        @Override
+        public void run() {
+            try {
+                try {
+                    final SRUClient c = client.get();
+                    c.setTimeQueued(System.nanoTime() - now);
+                    final S response = doRequest(c);
+                    callback.onSuccess(response);
+                } catch (SRUClientException e) {
+                    callback.onError(request, e);
+                }
+            } catch (Throwable t) {
+                callback.onError(request, new SRUClientException(
+                        "unexpected error while processing the request", t));
+            }
+        }
+
+
+        protected abstract S doRequest(SRUClient client)
+                throws SRUClientException;
+    }
+
+} // class SRUThreadedClient
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUVersion.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUVersion.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUVersion.java	(revision 2998)
@@ -0,0 +1,33 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * SRU version
+ */
+public enum SRUVersion {
+    /**
+     * SRU/CQL version 1.1
+     */
+    VERSION_1_1,
+
+    /**
+     * SRU/CQL version 1.2
+     */
+    VERSION_1_2
+
+} // enum SRUVersion
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUWhereInList.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUWhereInList.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUWhereInList.java	(revision 2998)
@@ -0,0 +1,44 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+/**
+ * A flag to indicate the position of the term within the complete term
+ * list.
+ */
+public enum SRUWhereInList {
+    /**
+     * The first term (<em>first</em>)
+     */
+    FIRST,
+
+    /**
+     * The last term (<em>last</em>)
+     */
+    LAST,
+
+    /**
+     * The only term (<em>only</em>)
+     */
+    ONLY,
+
+    /**
+     * Any other term (<em>inner</em>)
+     */
+    INNER
+
+} // enum SRUWhereInList
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUXMLStreamReader.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUXMLStreamReader.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/SRUXMLStreamReader.java	(revision 2998)
@@ -0,0 +1,836 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.namespace.QName;
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import org.codehaus.stax2.XMLInputFactory2;
+import org.codehaus.stax2.XMLStreamReader2;
+
+import com.ctc.wstx.api.WstxInputProperties;
+
+
+class SRUXMLStreamReader implements XMLStreamReader {
+    private final class CountingInputStream extends FilterInputStream {
+        protected long count = 0;
+
+        private CountingInputStream(InputStream stream) {
+            super(stream);
+        }
+
+        @Override
+        public int read() throws IOException {
+            final int value = super.read();
+            if (value != -1) {
+                count++;
+            }
+            return value;
+        }
+
+        @Override
+        public int read(byte[] buffer, int offset, int length)
+                throws IOException {
+            final int result = super.read(buffer, offset, length);
+            if (result >= 0) {
+                count += result;
+            }
+            return result;
+        }
+
+        @Override
+        public int read(byte[] buffer) throws IOException {
+            return this.read(buffer, 0, buffer.length);
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            final long result = super.skip(n);
+            if (result > 0) {
+                count += result;
+            }
+            return result;
+        }
+    } // class CountingInputStream
+    private static final XMLInputFactory2 factory;
+    private final InputStream stream;
+    private final XMLStreamReader2 reader;
+
+    SRUXMLStreamReader(InputStream in, boolean wrap) throws XMLStreamException {
+        this.stream = wrap ? new CountingInputStream(in) : in;
+        this.reader =
+                (XMLStreamReader2) factory.createXMLStreamReader(stream);
+    }
+
+
+    @Override
+    public void close() throws XMLStreamException {
+        reader.close();
+    }
+
+
+    @Override
+    public int getAttributeCount() {
+        return reader.getAttributeCount();
+    }
+
+
+    @Override
+    public String getAttributeLocalName(int index) {
+        return reader.getAttributeLocalName(index);
+    }
+
+
+    @Override
+    public QName getAttributeName(int index) {
+        return reader.getAttributeName(index);
+    }
+
+
+    @Override
+    public String getAttributeNamespace(int index) {
+        return reader.getAttributeNamespace(index);
+    }
+
+
+    @Override
+    public String getAttributePrefix(int index) {
+        return reader.getAttributePrefix(index);
+    }
+
+
+    @Override
+    public String getAttributeType(int index) {
+        return reader.getAttributeType(index);
+    }
+
+
+    @Override
+    public String getAttributeValue(int index) {
+        return reader.getAttributeValue(index);
+    }
+
+
+    @Override
+    public String getAttributeValue(String namespaceURI, String localName) {
+        return reader.getAttributeValue(namespaceURI, localName);
+    }
+
+
+    @Override
+    public String getCharacterEncodingScheme() {
+        return reader.getCharacterEncodingScheme();
+    }
+
+
+    @Override
+    public String getElementText() throws XMLStreamException {
+        return reader.getElementText();
+    }
+
+
+    @Override
+    public String getEncoding() {
+        return reader.getEncoding();
+    }
+
+
+    @Override
+    public int getEventType() {
+        return reader.getEventType();
+    }
+
+
+    @Override
+    public String getLocalName() {
+        return reader.getLocalName();
+    }
+
+
+    @Override
+    public Location getLocation() {
+        return reader.getLocation();
+    }
+
+
+    @Override
+    public QName getName() {
+        return reader.getName();
+    }
+
+
+    @Override
+    public NamespaceContext getNamespaceContext() {
+        return reader.getNamespaceContext();
+    }
+
+
+    @Override
+    public int getNamespaceCount() {
+        return reader.getNamespaceCount();
+    }
+
+
+    @Override
+    public String getNamespacePrefix(int index) {
+        return reader.getNamespacePrefix(index);
+    }
+
+
+    @Override
+    public String getNamespaceURI() {
+        return reader.getNamespaceURI();
+    }
+
+
+    @Override
+    public String getNamespaceURI(String prefix) {
+        return reader.getNamespaceURI(prefix);
+    }
+
+
+    @Override
+    public String getNamespaceURI(int index) {
+        return reader.getNamespaceURI(index);
+    }
+
+
+    @Override
+    public String getPIData() {
+        return reader.getPIData();
+    }
+
+
+    @Override
+    public String getPITarget() {
+        return reader.getPITarget();
+    }
+
+
+    @Override
+    public String getPrefix() {
+        return reader.getPrefix();
+    }
+
+
+    @Override
+    public Object getProperty(String name) throws IllegalArgumentException {
+        return reader.getProperty(name);
+    }
+
+
+    @Override
+    public String getText() {
+        return reader.getText();
+    }
+
+
+    @Override
+    public char[] getTextCharacters() {
+        return reader.getTextCharacters();
+    }
+
+
+    @Override
+    public int getTextCharacters(int sourceStart, char[] target,
+            int targetStart, int length) throws XMLStreamException {
+        return reader.getTextCharacters(sourceStart, target, targetStart, length);
+    }
+
+
+    @Override
+    public int getTextLength() {
+        return reader.getTextLength();
+    }
+
+
+    @Override
+    public int getTextStart() {
+        return reader.getTextStart();
+    }
+
+
+    @Override
+    public String getVersion() {
+        return reader.getVersion();
+    }
+
+
+    @Override
+    public boolean hasName() {
+        return reader.hasName();
+    }
+
+
+    @Override
+    public boolean hasNext() throws XMLStreamException {
+        return reader.hasNext();
+    }
+
+
+    @Override
+    public boolean hasText() {
+        return reader.hasText();
+    }
+
+
+    @Override
+    public boolean isAttributeSpecified(int index) {
+        return reader.isAttributeSpecified(index);
+    }
+
+
+    @Override
+    public boolean isCharacters() {
+        return reader.isCharacters();
+    }
+
+
+    @Override
+    public boolean isEndElement() {
+        return reader.isEndElement();
+    }
+
+
+    @Override
+    public boolean isStandalone() {
+        return reader.isStandalone();
+    }
+
+
+    @Override
+    public boolean isStartElement() {
+        return reader.isStartElement();
+    }
+
+
+    @Override
+    public boolean isWhiteSpace() {
+        return reader.isWhiteSpace();
+    }
+
+
+    @Override
+    public int next() throws XMLStreamException {
+        return reader.next();
+    }
+
+
+    @Override
+    public int nextTag() throws XMLStreamException {
+        return reader.nextTag();
+    }
+
+
+    @Override
+    public void require(int type, String namespaceURI, String localName)
+            throws XMLStreamException {
+        reader.require(type, namespaceURI, localName);
+    }
+
+
+    @Override
+    public boolean standaloneSet() {
+        return reader.standaloneSet();
+    }
+
+
+    void closeCompletly() {
+        try {
+            reader.close();
+        } catch (XMLStreamException e) {
+            /* IGNORE */
+        }
+        try {
+            stream.close();
+        } catch (IOException e) {
+            /* IGNORE */
+        }
+    }
+
+
+    long getByteCount() {
+        if (stream instanceof CountingInputStream) {
+            return ((CountingInputStream) stream).count;
+        } else {
+            return -1;
+        }
+    }
+
+
+    boolean readStart(String namespaceURI, String localName, boolean required)
+            throws XMLStreamException {
+        return readStart(namespaceURI, localName, required, false);
+    }
+
+
+    boolean readStart(String namespaceURI, String localName, boolean required,
+            boolean attributes) throws XMLStreamException {
+        // System.err.println("readStart (" + localName + ", required = " +
+        // required + ") @ " + toReadable(reader));
+        if (!reader.isEndElement()) {
+            while (reader.hasNext()) {
+                // System.err.println("  LOOP: " + dumpState());
+                if (reader.isWhiteSpace()) {
+                    reader.next();
+                    continue;
+                }
+                if (reader.isStartElement()) {
+                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
+                            localName.equals(reader.getLocalName())) {
+                        // System.err.print("--> found ");
+                        if (!attributes) {
+                            // System.err.print("and consumed ");
+                            reader.next(); // skip to next event
+                        }
+                        // System.err.println("@ " + toReadable(reader));
+                        return true;
+                    }
+                    break;
+                }
+                if (reader.isCharacters() || reader.isEndElement()) {
+                    break;
+                }
+                reader.next();
+            } // while
+        }
+        if (required) {
+            // System.err.println("--> error, not found @ " +
+            // toReadable(reader));
+            throw new XMLStreamException("expected element '" +
+                    new QName(namespaceURI, localName) + "', but found '" +
+                    reader.getName() + "'", reader.getLocation());
+        }
+        // System.err.println("--> not found @ " + toReadable(reader));
+        return false;
+    }
+
+
+    void readEnd(String namespaceURI, String localName)
+            throws XMLStreamException {
+        readEnd(namespaceURI, localName, false);
+    }
+
+
+    void readEnd(String namespaceURI, String localName, boolean skipContent)
+            throws XMLStreamException {
+        // System.err.println("readEnd (" + localName + ") @ " + dumpState() +
+        // ", skipContent = " + skipContent);
+        int level = 1;
+        while (reader.hasNext()) {
+            // System.err.println("  LOOP " + dumpState() + " [" +
+            // level + "]");
+            if (reader.isWhiteSpace()) {
+                reader.next();
+                continue;
+            }
+            if (skipContent) {
+                if (reader.isCharacters()) {
+                    reader.next();
+                    continue;
+                }
+                if (reader.isStartElement()) {
+                    if (!(namespaceURI.equals(reader.getNamespaceURI()) && localName
+                            .equals(reader.getLocalName()))) {
+                        level++;
+                    }
+                    reader.next();
+                    continue;
+                }
+            }
+            if (reader.isEndElement()) {
+                level--;
+                // System.err.println("   @END-TAG: " + dumpState() + " [" +
+                // level + "]");
+                if (level == 0) {
+                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
+                            localName.equals(reader.getLocalName())) {
+                        reader.next(); // consume tag
+                        break;
+                    } else {
+                        throw new XMLStreamException("expected end tag for '" +
+                                new QName(namespaceURI, localName) +
+                                "', but found '" + reader.getName() + "'",
+                                reader.getLocation());
+                    }
+                }
+            }
+            reader.next();
+        }
+        // System.err.println("--> ok @ " + dumpState());
+    }
+
+
+    boolean peekStart(String namespaceURI, String localName)
+            throws XMLStreamException {
+        if (!reader.isEndElement()) {
+            while (reader.hasNext()) {
+                // System.err.println("  LOOP: " + dumpState());
+                if (reader.isWhiteSpace()) {
+                    reader.next();
+                    continue;
+                }
+                if (reader.isStartElement()) {
+                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
+                            localName.equals(reader.getLocalName())) {
+                        return true;
+                    } else {
+                        return false;
+                    }
+                }
+                if (reader.isCharacters() || reader.isEndElement()) {
+                    break;
+                }
+                reader.next();
+            } // while
+        }
+        return false;
+    }
+
+
+    String readContent(String namespaceURI, String localName, boolean required)
+            throws XMLStreamException {
+        return readContent(namespaceURI, localName, required, true);
+    }
+
+        
+    String readContent(String namespaceURI, String localName, boolean required,
+            boolean contentRequired) throws XMLStreamException {
+        String result = null;
+        if (readStart(namespaceURI, localName, required)) {
+            try {
+                result = readString(contentRequired);
+                if (!contentRequired && (result == null)) {
+                    result = "";
+                }
+            } catch (XMLStreamException e) {
+                StringBuilder sb = new StringBuilder();
+                sb.append("element '");
+                if (namespaceURI != null) {
+                    sb.append('{').append(namespaceURI).append('}');
+                }
+                sb.append(localName).append("' may not be empty");
+                throw new XMLStreamException(sb.toString(), e.getLocation());
+            }
+            readEnd(namespaceURI, localName);
+        }
+        return result;
+    }
+
+
+    int readContent(String namespaceURI, String localName, boolean required,
+            int defaultValue) throws XMLStreamException {
+        if (readStart(namespaceURI, localName, required)) {
+            String s = readString(true);
+            try {
+                readEnd(namespaceURI, localName);
+            } catch (XMLStreamException e) {
+                StringBuilder sb = new StringBuilder();
+                sb.append("element '");
+                if (namespaceURI != null) {
+                    sb.append('{').append(namespaceURI).append('}');
+                }
+                sb.append(localName).append("' may not be empty");
+                throw new XMLStreamException(sb.toString(), e.getLocation());
+            }
+            try {
+                return Integer.parseInt(s);
+            } catch (NumberFormatException e) {
+                StringBuilder sb = new StringBuilder();
+                sb.append("element '");
+                if (namespaceURI != null) {
+                    sb.append('{').append(namespaceURI).append('}');
+                }
+                sb.append(" was expected to be of type xs:integer; ");
+                sb.append("incompatible value was: ").append(s);
+                throw new XMLStreamException(sb.toString(),
+                        reader.getLocation(), e);
+            }
+        }
+        return defaultValue;
+    }
+
+
+    String readString(boolean required) throws XMLStreamException {
+        // System.err.println("readString @ " + toReadable(reader));
+        StringBuilder sb = new StringBuilder();
+        while (reader.isCharacters()) {
+            String s = reader.getText();
+            if (s != null) {
+                sb.append(s);
+            }
+            reader.next();
+        } // while
+        String s = null;
+        if (sb.length() > 0) {
+            s = sb.toString().trim();
+        }
+        if (required && ((s == null) || s.isEmpty())) {
+            throw new XMLStreamException("expected character content "
+                    + "at position ", reader.getLocation());
+        }
+        // System.err.println("--> ok @ " + toReadable(reader));
+        return s;
+    }
+
+
+    String readAttributeValue(String namespaceURI, String localName)
+            throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start elment event",
+                    reader.getLocation());
+        }
+        String attr = reader.getAttributeValue(namespaceURI, localName);
+        if (attr != null) {
+            attr = attr.trim().intern();
+        }
+        return attr;
+    }
+
+
+    String readNamespaceURI() throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start elment event",
+                    reader.getLocation());
+        }
+        return reader.getNamespaceURI();
+    }
+
+
+    String peekElementLocalName() throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start elment event",
+                    reader.getLocation());
+        }
+        return reader.getLocalName();
+    }
+
+
+    void consumeStart() throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start elment event",
+                    reader.getLocation());
+        }
+        reader.next();
+    }
+
+
+    void consumeWhitespace() throws XMLStreamException {
+        while (reader.isWhiteSpace() && reader.hasNext()) {
+            reader.next();
+            continue;
+        }
+    }
+
+
+    void copyTo(XMLStreamWriter writer) throws XMLStreamException {
+        final int depth = reader.getDepth();
+        do {
+            copyEvent(reader, writer);
+            reader.next();
+        } while (reader.getDepth() >= depth);
+    }
+
+
+    String dumpState() {
+        StringBuilder sb = new StringBuilder();
+        switch (reader.getEventType()) {
+        case XMLStreamConstants.START_DOCUMENT:
+            return "START_DOC";
+        case XMLStreamConstants.END_DOCUMENT:
+            return "END_DOC";
+        case XMLStreamConstants.START_ELEMENT:
+            sb.append("START[");
+            sb.append(reader.getNamespaceURI());
+            sb.append(",");
+            sb.append(reader.getLocalName());
+            sb.append("]");
+            break;
+        case XMLStreamConstants.END_ELEMENT:
+            sb.append("END[");
+            sb.append(reader.getNamespaceURI());
+            sb.append(",");
+            sb.append(reader.getLocalName());
+            sb.append("]");
+            break;
+        case XMLStreamConstants.CHARACTERS:
+            sb.append("CHARACTERS[\"");
+            sb.append(reader.getText().replace("\n", "\\n")
+                    .replace("\r", "\\r").replace("\t", "\\t"));
+            sb.append("\", isWhitespace = ");
+            sb.append(reader.isWhiteSpace());
+            sb.append("]");
+            break;
+        case XMLStreamConstants.CDATA:
+            sb.append("CDATA[\"");
+            sb.append(reader.getText().replace("\n", "\\n")
+                    .replace("\r", "\\r").replace("\t", "\\t"));
+            sb.append("\", isWhitespace = ");
+            sb.append(reader.isWhiteSpace());
+            sb.append("]");
+            break;
+        default:
+            sb.append(Integer.toString(reader.getEventType()));
+        }
+        return sb.toString();
+    }
+
+
+    private static void copyEvent(XMLStreamReader from, XMLStreamWriter to)
+            throws XMLStreamException {
+        switch (from.getEventType()) {
+        case XMLStreamConstants.START_DOCUMENT:
+            {
+                String version = from.getVersion();
+                if (version == null || version.length() == 0) {
+                    to.writeStartDocument();
+                } else {
+                    to.writeStartDocument(from.getCharacterEncodingScheme(),
+                            from.getVersion());
+                }
+                to.writeCharacters("\n");
+            }
+            return;
+
+        case XMLStreamConstants.END_DOCUMENT:
+            to.writeCharacters("\n");
+            to.writeEndDocument();
+            return;
+
+        case XMLStreamConstants.START_ELEMENT:
+            copyStartElement(from, to);
+            return;
+
+        case XMLStreamConstants.END_ELEMENT:
+            to.writeEndElement();
+            return;
+
+        case XMLStreamConstants.SPACE:
+            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
+                    from.getTextLength());
+            return;
+
+        case XMLStreamConstants.CDATA:
+            to.writeCData(from.getText());
+            return;
+
+        case XMLStreamConstants.CHARACTERS:
+            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
+                    from.getTextLength());
+            return;
+
+        case XMLStreamConstants.COMMENT:
+            to.writeComment(from.getText());
+            return;
+
+        case XMLStreamConstants.PROCESSING_INSTRUCTION:
+            to.writeProcessingInstruction(from.getPITarget(), from.getPIData());
+            return;
+
+        case XMLStreamConstants.DTD:
+        case XMLStreamConstants.ENTITY_REFERENCE:
+        case XMLStreamConstants.ATTRIBUTE:
+        case XMLStreamConstants.NAMESPACE:
+        case XMLStreamConstants.ENTITY_DECLARATION:
+        case XMLStreamConstants.NOTATION_DECLARATION:
+            /* FALL_TROUGH */
+        }
+        throw new XMLStreamException("unsupported event type: " +
+                from.getEventType());
+    }
+
+
+    private static void copyStartElement(XMLStreamReader from,
+            XMLStreamWriter to) throws XMLStreamException {
+        final int nsCount = from.getNamespaceCount();
+        if (nsCount > 0) { // yup, got some...
+            for (int i = 0; i < nsCount; ++i) {
+                String pfx = from.getNamespacePrefix(i);
+                String uri = from.getNamespaceURI(i);
+                if ((pfx == null) || pfx.isEmpty()) { // default NS
+                    to.setDefaultNamespace(uri);
+                } else {
+                    to.setPrefix(pfx, uri);
+                }
+            }
+        }
+
+        final String prefix             = from.getPrefix();
+        final NamespaceContext from_ctx = from.getNamespaceContext();
+        final NamespaceContext to_ctx   = to.getNamespaceContext();
+        boolean repair_prefix_namespace = false;
+        if ((prefix != null) && (to_ctx.getNamespaceURI(prefix) == null)) {
+            repair_prefix_namespace = true;
+            to.setPrefix(prefix, from_ctx.getNamespaceURI(prefix));
+        }
+
+        to.writeStartElement(prefix, from.getLocalName(),
+                from.getNamespaceURI());
+
+        if (nsCount > 0) {
+            // write namespace declarations
+            for (int i = 0; i < nsCount; ++i) {
+                String pfx = from.getNamespacePrefix(i);
+                String uri    = from.getNamespaceURI(i);
+
+                if ((pfx == null) || pfx.isEmpty()) { // default NS
+                    to.writeDefaultNamespace(uri);
+                } else {
+                    to.writeNamespace(pfx, uri);
+                }
+            }
+        }
+        if (repair_prefix_namespace) {
+            to.writeNamespace(prefix, from_ctx.getNamespaceURI(prefix));
+        }
+
+        int attrCount = from.getAttributeCount();
+        if (attrCount > 0) {
+            for (int i = 0; i < attrCount; ++i) {
+                to.writeAttribute(from.getAttributePrefix(i),
+                        from.getAttributeNamespace(i),
+                        from.getAttributeLocalName(i),
+                        from.getAttributeValue(i));
+            }
+        }
+    }
+
+
+    static {
+        factory = (XMLInputFactory2) XMLInputFactory.newInstance();
+        // Stax settings
+        factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.TRUE);
+
+        // Stax2 settings
+        factory.setProperty(XMLInputFactory2.P_INTERN_NS_URIS, Boolean.TRUE);
+        factory.setProperty(XMLInputFactory2.P_LAZY_PARSING, Boolean.FALSE);
+
+        // Woodstox settings
+        factory.setProperty(WstxInputProperties.P_NORMALIZE_LFS, Boolean.TRUE);
+    }
+
+} // SRUXMLStreamReader
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/XmlStreamReaderProxy.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/XmlStreamReaderProxy.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/XmlStreamReaderProxy.java	(revision 2998)
@@ -0,0 +1,545 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.namespace.QName;
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+class XmlStreamReaderProxy implements XMLStreamReader {
+    private static enum State {
+        START_DOCUMENT, PROCESSING, END_DOCUMENT, PAST_END_DOCUMENT
+    }
+
+    private XMLStreamReader reader;
+    private State state;
+    private int depth;
+
+    XmlStreamReaderProxy() {
+        super();
+    }
+
+    void reset(XMLStreamReader reader) throws XMLStreamException {
+        this.reader = reader;
+        this.state = State.START_DOCUMENT;
+        this.depth = 0;
+    }
+
+    @Override
+    public void close() throws XMLStreamException {
+    }
+
+    @Override
+    public int getAttributeCount() {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeCount();
+    }
+
+    @Override
+    public String getAttributeLocalName(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeLocalName(index);
+    }
+
+    @Override
+    public QName getAttributeName(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeName(index);
+    }
+
+    @Override
+    public String getAttributeNamespace(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeNamespace(index);
+    }
+
+    @Override
+    public String getAttributePrefix(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributePrefix(index);
+    }
+
+    @Override
+    public String getAttributeType(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeType(index);
+    }
+
+    @Override
+    public String getAttributeValue(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeValue(index);
+    }
+
+    @Override
+    public String getAttributeValue(String namespaceURI, String localName) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.getAttributeValue(namespaceURI, localName);
+    }
+
+    @Override
+    public String getCharacterEncodingScheme() {
+        return reader.getCharacterEncodingScheme();
+    }
+
+    @Override
+    public String getElementText() throws XMLStreamException {
+        if (!checkEventType(XMLStreamConstants.START_ELEMENT)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT");
+        }
+        return reader.getElementText();
+    }
+
+    @Override
+    public String getEncoding() {
+        return reader.getEncoding();
+    }
+
+    @Override
+    public int getEventType() {
+        switch (state) {
+        case START_DOCUMENT:
+            return XMLStreamConstants.START_DOCUMENT;
+        case PROCESSING:
+            return reader.getEventType();
+        case END_DOCUMENT:
+            return XMLStreamConstants.END_DOCUMENT;
+        case PAST_END_DOCUMENT:
+            throw new IllegalStateException("past end of document");
+        default:
+            throw new RuntimeException("invalid internel state: " + state);
+        }
+    }
+
+    @Override
+    public String getLocalName() {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT,
+                XMLStreamConstants.ENTITY_REFERENCE)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT, END_ELEMENT or ENTITY_REFERENCE");
+        }
+        return reader.getLocalName();
+    }
+
+    @Override
+    public Location getLocation() {
+        return reader.getLocation();
+    }
+
+    @Override
+    public QName getName() {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT or END_ELEMENT");
+        }
+        return reader.getName();
+    }
+
+    @Override
+    public NamespaceContext getNamespaceContext() {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT, END_ELEMENT or NAMESPACE");
+        }
+        return reader.getNamespaceContext();
+    }
+
+    @Override
+    public int getNamespaceCount() {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT, END_ELEMENT or NAMESPACE");
+        }
+        return reader.getNamespaceCount();
+    }
+
+    @Override
+    public String getNamespacePrefix(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT, END_ELEMENT or NAMESPACE");
+        }
+        return reader.getNamespacePrefix(index);
+    }
+
+    @Override
+    public String getNamespaceURI() {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT, END_ELEMENT or NAMESPACE");
+        }
+        return reader.getNamespaceURI();
+    }
+
+    @Override
+    public String getNamespaceURI(String prefix) {
+        return reader.getNamespaceURI(prefix);
+    }
+
+    @Override
+    public String getNamespaceURI(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
+            throw new IllegalStateException("Current event is not "
+                    + "START_ELEMENT, END_ELEMENT or NAMESPACE");
+        }
+        return reader.getNamespaceURI(index);
+    }
+
+    @Override
+    public String getPIData() {
+        if (!checkEventType(XMLStreamConstants.PROCESSING_INSTRUCTION)) {
+            throw new IllegalStateException(
+                    "Current event is not PROCESSING_INSTRUCTION");
+        }
+        return reader.getPIData();
+    }
+
+    @Override
+    public String getPITarget() {
+        if (!checkEventType(XMLStreamConstants.PROCESSING_INSTRUCTION)) {
+            throw new IllegalStateException(
+                    "Current event is not PROCESSING_INSTRUCTION");
+        }
+        return reader.getPITarget();
+    }
+
+    @Override
+    public String getPrefix() {
+        return reader.getPrefix();
+    }
+
+    @Override
+    public Object getProperty(String name) throws IllegalArgumentException {
+        return reader.getProperty(name);
+    }
+
+    @Override
+    public String getText() {
+        if (!checkEventTypes(XMLStreamConstants.CHARACTERS,
+                XMLStreamConstants.CDATA, XMLStreamConstants.COMMENT,
+                XMLStreamConstants.SPACE, XMLStreamConstants.ENTITY_REFERENCE)) {
+            throw new IllegalStateException("Current event is a text event");
+        }
+        return reader.getText();
+    }
+
+    @Override
+    public char[] getTextCharacters() {
+        if (!checkEventTypes(XMLStreamConstants.CHARACTERS,
+                XMLStreamConstants.CDATA, XMLStreamConstants.COMMENT,
+                XMLStreamConstants.SPACE)) {
+            throw new IllegalStateException("Current event is a text event");
+        }
+        return reader.getTextCharacters();
+    }
+
+    @Override
+    public int getTextCharacters(int sourceStart, char[] target,
+            int targetStart, int length) throws XMLStreamException {
+        if (!checkEventTypes(XMLStreamConstants.CHARACTERS,
+                XMLStreamConstants.CDATA, XMLStreamConstants.COMMENT,
+                XMLStreamConstants.SPACE)) {
+            throw new UnsupportedOperationException(
+                    "Current event is a text event");
+        }
+        return reader.getTextCharacters(sourceStart, target, targetStart,
+                length);
+    }
+
+    @Override
+    public int getTextLength() {
+        if (!checkEventTypes(XMLStreamConstants.CHARACTERS,
+                XMLStreamConstants.CDATA, XMLStreamConstants.COMMENT,
+                XMLStreamConstants.SPACE)) {
+            throw new IllegalStateException("invalid state");
+        }
+        return reader.getTextLength();
+    }
+
+    @Override
+    public int getTextStart() {
+        if (!checkEventTypes(XMLStreamConstants.CHARACTERS,
+                XMLStreamConstants.CDATA, XMLStreamConstants.COMMENT,
+                XMLStreamConstants.SPACE)) {
+            throw new IllegalStateException("invalid state");
+        }
+        return reader.getTextStart();
+    }
+
+    @Override
+    public String getVersion() {
+        return reader.getVersion();
+    }
+
+    @Override
+    public boolean hasName() {
+        switch (state) {
+        case START_DOCUMENT:
+            /* FALL-THOUGH */
+        case END_DOCUMENT:
+            /* FALL-THOUGH */
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.hasName();
+        }
+    }
+
+    @Override
+    public boolean hasNext() throws XMLStreamException {
+        if (state == State.END_DOCUMENT) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean hasText() {
+        switch (state) {
+        case START_DOCUMENT:
+            /* FALL-THOUGH */
+        case END_DOCUMENT:
+            /* FALL-THOUGH */
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.hasText();
+        } // switch
+    }
+
+    @Override
+    public boolean isAttributeSpecified(int index) {
+        if (!checkEventTypes(XMLStreamConstants.START_ELEMENT,
+                XMLStreamConstants.ATTRIBUTE)) {
+            throw new IllegalStateException(
+                    "Current event is not START_ELEMENT or ATTRIBUTE");
+        }
+        return reader.isAttributeSpecified(index);
+    }
+
+    @Override
+    public boolean isCharacters() {
+        switch (state) {
+        case START_DOCUMENT:
+            /* FALL-THOUGH */
+        case END_DOCUMENT:
+            /* FALL-THOUGH */
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.isCharacters();
+        }
+    }
+
+    @Override
+    public boolean isEndElement() {
+        switch (state) {
+        case START_DOCUMENT:
+            /* FALL-THOUGH */
+        case END_DOCUMENT:
+            /* FALL-THOUGH */
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.isEndElement();
+        }
+    }
+
+    @Override
+    public boolean isStandalone() {
+        return reader.isStandalone();
+    }
+
+    @Override
+    public boolean isStartElement() {
+        switch (state) {
+        case START_DOCUMENT:
+            /* FALL-THOUGH */
+        case END_DOCUMENT:
+            /* FALL-THOUGH */
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.isStartElement();
+        }
+    }
+
+    @Override
+    public boolean isWhiteSpace() {
+        switch (state) {
+        case START_DOCUMENT:
+            /* FALL-THOUGH */
+        case END_DOCUMENT:
+            return true;
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.isWhiteSpace();
+        }
+    }
+
+    @Override
+    public int next() throws XMLStreamException {
+        int type = -1;
+        switch (state) {
+        case START_DOCUMENT:
+            state = State.PROCESSING;
+            type = reader.getEventType();
+            if (type == XMLStreamConstants.START_ELEMENT) {
+                depth++;
+            }
+            break;
+        case PROCESSING:
+            type = reader.next();
+            switch (type) {
+            case XMLStreamConstants.START_ELEMENT:
+                depth++;
+                break;
+            case XMLStreamConstants.END_ELEMENT:
+                depth--;
+                break;
+            default:
+                ;
+            } // switch (t)
+            if (depth < 0) {
+                state = State.END_DOCUMENT;
+                type = XMLStreamConstants.END_DOCUMENT;
+            }
+            break;
+        case END_DOCUMENT:
+            state = State.PAST_END_DOCUMENT;
+            type = XMLStreamConstants.END_DOCUMENT;
+            break;
+        default:
+            throw new IllegalStateException("past end of input");
+        } // switch (state)
+        return type;
+    }
+
+    @Override
+    public int nextTag() throws XMLStreamException {
+        boolean skip = true;
+        int type = getEventType();
+        do {
+            switch (type) {
+            case XMLStreamConstants.CHARACTERS:
+                /* FALL_TROUGH */
+            case XMLStreamConstants.CDATA:
+                if (!isWhiteSpace()) {
+                    skip = false;
+                    break;
+                }
+            case XMLStreamConstants.START_DOCUMENT:
+                /* FALL_TROUGH */
+            case XMLStreamConstants.SPACE:
+                /* FALL_TROUGH */
+            case XMLStreamConstants.PROCESSING_INSTRUCTION:
+                /* FALL_TROUGH */
+            case XMLStreamConstants.COMMENT:
+                type = next();
+                break;
+            default:
+                skip = false;
+            } // switch
+        } while (skip);
+        if ((type != XMLStreamConstants.START_ELEMENT) &&
+                (type != XMLStreamConstants.END_ELEMENT)) {
+            throw new XMLStreamException("expected start or end tag",
+                    getLocation());
+        }
+        return type;
+    }
+
+    @Override
+    public void require(int type, String namespaceURI, String localName)
+            throws XMLStreamException {
+        reader.require(type, namespaceURI, localName);
+    }
+
+    @Override
+    public boolean standaloneSet() {
+        return reader.standaloneSet();
+    }
+
+    private boolean checkEventType(int type) {
+        switch (state) {
+        case START_DOCUMENT:
+            return XMLStreamConstants.START_DOCUMENT == type;
+        case END_DOCUMENT:
+            return XMLStreamConstants.END_DOCUMENT == type;
+        case PAST_END_DOCUMENT:
+            return false;
+        default:
+            return reader.getEventType() == type;
+        } // switch
+    }
+
+    private boolean checkEventTypes(int... types) {
+        if (state == State.PROCESSING) {
+            for (int type : types) {
+                if (type == reader.getEventType()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+} // class XmlStreamReaderWrapper
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/XmlStreamReaderUtils.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/XmlStreamReaderUtils.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/XmlStreamReaderUtils.java	(revision 2998)
@@ -0,0 +1,260 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+
+/**
+ * Helper class for dealing with {@link XMLStreamReader}.
+ * <p>
+ * <em>Note: this class is semi-public API and may be change or removed in the future.</em>
+ * </p>
+ */
+public final class XmlStreamReaderUtils {
+    private XmlStreamReaderUtils() {
+    }
+
+
+    public static boolean readStart(XMLStreamReader reader,
+            String namespaceURI, String localName, boolean required)
+            throws XMLStreamException {
+        return readStart(reader, namespaceURI, localName, required, false);
+    }
+
+
+    public static boolean readStart(XMLStreamReader reader,
+            String namespaceURI, String localName, boolean required,
+            boolean attributes) throws XMLStreamException {
+        // System.err.println("readStart (" + localName + ", required = " +
+        // required + ") @ " + toReadable(reader));
+        if (!reader.isEndElement()) {
+            while (reader.hasNext()) {
+                // System.err.println("  LOOP: " + dumpState());
+                if (reader.isWhiteSpace()) {
+                    reader.next();
+                    continue;
+                }
+                if (reader.isStartElement()) {
+                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
+                            localName.equals(reader.getLocalName())) {
+                        // System.err.print("--> found ");
+                        if (!attributes) {
+                            // System.err.print("and consumed ");
+                            reader.next(); // skip to next event
+                        }
+                        // System.err.println("@ " + toReadable(reader));
+                        return true;
+                    }
+                    break;
+                }
+                if (reader.isCharacters() || reader.isEndElement()) {
+                    break;
+                }
+                reader.next();
+            } // while
+        }
+        if (required) {
+            // System.err.println("--> error, not found @ " +
+            // toReadable(reader));
+            throw new XMLStreamException("expected element '" +
+                    new QName(namespaceURI, localName) + "', but found '" +
+                    reader.getName() + "'", reader.getLocation());
+        }
+        // System.err.println("--> not found @ " + toReadable(reader));
+        return false;
+    }
+
+
+    public static void readEnd(XMLStreamReader reader, String namespaceURI,
+            String localName) throws XMLStreamException {
+        readEnd(reader, namespaceURI, localName, false);
+    }
+
+
+    public static void readEnd(XMLStreamReader reader, String namespaceURI,
+            String localName, boolean skipContent) throws XMLStreamException {
+        // System.err.println("readEnd (" + localName + ") @ " + dumpState() +
+        // ", skipContent = " + skipContent);
+        int level = 1;
+        while (reader.hasNext()) {
+            // System.err.println("  LOOP " + dumpState() + " [" +
+            // level + "]");
+            if (reader.isWhiteSpace()) {
+                reader.next();
+                continue;
+            }
+            if (skipContent) {
+                if (reader.isCharacters()) {
+                    reader.next();
+                    continue;
+                }
+                if (reader.isStartElement()) {
+                    if (!(namespaceURI.equals(reader.getNamespaceURI()) &&
+                            localName.equals(reader.getLocalName()))) {
+                        level++;
+                    }
+                    reader.next();
+                    continue;
+                }
+            }
+            if (reader.isEndElement()) {
+                level--;
+                // System.err.println("   @END-TAG: " + dumpState() + " [" +
+                // level + "]");
+                if (level == 0) {
+                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
+                            localName.equals(reader.getLocalName())) {
+                        reader.next(); // consume tag
+                        break;
+                    } else {
+                        throw new XMLStreamException("expected end tag for '" +
+                                new QName(namespaceURI, localName) +
+                                "', but found '" + reader.getName() + "'",
+                                reader.getLocation());
+                    }
+                }
+            }
+            reader.next();
+        }
+        // System.err.println("--> ok @ " + dumpState());
+    }
+
+
+    public static String readContent(XMLStreamReader reader,
+            String namespaceURI, String localName, boolean required)
+            throws XMLStreamException {
+        String result = null;
+        if (readStart(reader, namespaceURI, localName, required)) {
+            result = readString(reader, true);
+            readEnd(reader, namespaceURI, localName);
+        }
+        return result;
+    }
+
+
+    public static int readContent(XMLStreamReader reader, String namespaceURI,
+            String localName, boolean required, int defaultValue)
+            throws XMLStreamException {
+        if (readStart(reader, namespaceURI, localName, required)) {
+            String s = readString(reader, true);
+            readEnd(reader, namespaceURI, localName);
+            try {
+                return Integer.parseInt(s);
+            } catch (NumberFormatException e) {
+                throw new XMLStreamException(
+                        "expected a xs:integer value: s", e);
+            }
+        }
+        return defaultValue;
+    }
+
+
+    public static String readString(XMLStreamReader reader, boolean required)
+            throws XMLStreamException {
+        // System.err.println("readString @ " + toReadable(reader));
+        String s = null;
+        if (reader.isCharacters()) {
+            s = reader.getText();
+            if (s != null) {
+                s = s.trim();
+            }
+            reader.next();
+        }
+        if (required && ((s == null) || s.isEmpty())) {
+            throw new XMLStreamException("expected character content "
+                    + "at position", reader.getLocation());
+        }
+        // System.err.println("--> ok @ " + toReadable(reader));
+        return s;
+    }
+
+
+    public static String readAttributeValue(XMLStreamReader reader,
+            String namespaceURI, String localName, boolean required)
+            throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start elment event",
+                    reader.getLocation());
+        }
+        String attr = reader.getAttributeValue(namespaceURI, localName);
+        if (attr != null) {
+            attr = attr.trim();
+            if (attr.isEmpty()) {
+                attr = null;
+            }
+        }
+        if ((attr == null) && required) {
+            throw new XMLStreamException("expected non-empty attribute '" +
+                    new QName(namespaceURI, localName) + "' on element '" +
+                    reader.getName() + "'", reader.getLocation());
+        }
+        return attr;
+    }
+
+
+    public static String readAttributeValue(XMLStreamReader reader,
+            String namespaceURI, String localName) throws XMLStreamException {
+        return readAttributeValue(reader, namespaceURI, localName, false);
+    }
+
+
+    public static String readNamespaceURI(XMLStreamReader reader)
+            throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start elment event",
+                    reader.getLocation());
+        }
+        return reader.getNamespaceURI();
+    }
+
+
+    public static boolean peekStart(XMLStreamReader reader,
+            String namespaceURI, String localName)
+            throws XMLStreamException {
+        if (reader.isWhiteSpace()) {
+            consumeWhitespace(reader);
+        }
+        if (!reader.isStartElement()) {
+            return false;
+        }
+        return namespaceURI.equals(reader.getNamespaceURI()) &&
+                localName.equals(reader.getLocalName());
+    }
+    
+    
+    public static void consumeStart(XMLStreamReader reader)
+            throws XMLStreamException {
+        if (!reader.isStartElement()) {
+            throw new XMLStreamException("not at a start element event",
+                    reader.getLocation());
+        }
+        reader.next();
+    }
+
+
+    public static void consumeWhitespace(XMLStreamReader reader)
+            throws XMLStreamException {
+        while (reader.isWhiteSpace() && reader.hasNext()) {
+            reader.next();
+            continue;
+        }
+    }
+
+} // class XmlStreamReaderUtils
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordData.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordData.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordData.java	(revision 2998)
@@ -0,0 +1,63 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.List;
+
+import eu.clarin.sru.client.SRURecordData;
+
+
+/**
+ * A record data implementation for CLARIN-FCS.
+ */
+public final class ClarinFCSRecordData implements
+        SRURecordData {
+    /**
+     * The record schema for CLARIN-FCS records.
+     */
+    public static final String RECORD_SCHEMA = "http://clarin.eu/fcs/1.0";
+    private final Resource resource;
+
+
+    ClarinFCSRecordData(String pid, String ref,
+            List<DataView> dataviews,
+            List<Resource.ResourceFragment> resourceFragments) {
+        this.resource = new Resource(pid, ref, dataviews, resourceFragments);
+    }
+
+
+    @Override
+    public boolean isTransient() {
+        return false;
+    }
+
+
+    @Override
+    public String getRecordSchema() {
+        return RECORD_SCHEMA;
+    }
+
+    /**
+     * Get the CLARIN-FCS record resource.
+     * 
+     * @return a {@link Resource} object
+     */
+    public Resource getResource() {
+        return resource;
+    }
+
+} // class ClarinFederatedContentSearchRecordData
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordParser.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordParser.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordParser.java	(revision 2998)
@@ -0,0 +1,154 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRURecordData;
+import eu.clarin.sru.client.SRURecordDataParser;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+
+
+/**
+ * A record parse to parse records conforming to CLARIN-FCS specification.
+ */
+public class ClarinFCSRecordParser implements SRURecordDataParser {
+    private static final Logger logger =
+            LoggerFactory.getLogger(ClarinFCSRecordParser.class);
+    private static final String FCS_NS =
+            ClarinFCSRecordData.RECORD_SCHEMA;
+    // TODO: make this configurable
+    private final DataViewParser[] parsers = new DataViewParser[] {
+            new DataViewParserGenericDOM(),
+//            new DataViewParserGenericString(),
+            new DataViewParserKWIC()
+    };
+
+
+    @Override
+    public String getRecordSchema() {
+        return ClarinFCSRecordData.RECORD_SCHEMA;
+    }
+
+
+    @Override
+    public SRURecordData parse(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+        logger.debug("parsing CLARIN-FCS record");
+
+        // Resource
+        XmlStreamReaderUtils.readStart(reader, FCS_NS, "Resource", true, true);
+        String pid = XmlStreamReaderUtils.readAttributeValue(reader, null, "pid");
+        String ref = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+        XmlStreamReaderUtils.consumeStart(reader);
+
+        // Resource/Resource (optional)
+        if (XmlStreamReaderUtils.readStart(reader, FCS_NS, "Resource", false)) {
+            logger.info("skipping nested <Resource> element");
+            XmlStreamReaderUtils.readEnd(reader, FCS_NS, "Resource", true);
+        }
+
+        // Resource/DataView
+        final List<DataView> dataviews = parseDataViews(reader);
+
+        // Resource/ResourceFragment
+        final List<Resource.ResourceFragment> resourceFragments =
+                parseResourceFragments(reader);
+
+        XmlStreamReaderUtils.readEnd(reader, FCS_NS, "Resource", true);
+
+        return new ClarinFCSRecordData(pid, ref, dataviews, resourceFragments);
+    }
+
+
+    private List<DataView> parseDataViews(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+        List<DataView> dataviews = null;
+
+        while (XmlStreamReaderUtils.readStart(reader, FCS_NS, "DataView", false, true)) {
+            String pid = XmlStreamReaderUtils.readAttributeValue(reader, null, "pid");
+            String ref = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+            String type = XmlStreamReaderUtils.readAttributeValue(reader, null, "type");
+            if ((type == null) || type.isEmpty()) {
+                throw new SRUClientException("element <DataView> needs a "
+                        + "non-empty 'type' attribute");
+            }
+
+            // consume start element and get rid of any whitespace
+            XmlStreamReaderUtils.consumeStart(reader);
+            XmlStreamReaderUtils.consumeWhitespace(reader);
+
+            logger.debug("processing <DataView> of type = {}", type);
+
+            DataViewParser parser = null;
+            for (int i = 0; i < parsers.length; i++) {
+                if (parsers[i].acceptType(type) &&
+                        ((parser == null) ||
+                         (parser.getPriority() < parsers[i].getPriority()))) {
+                    parser = parsers[i];
+                }
+            }
+
+            DataView dataview = null;
+            if (parser != null) {
+                dataview = parser.parse(reader, type, pid, ref);
+            } else {
+                logger.warn("no parser found for <DataView> of type = {}", type);
+            }
+
+            XmlStreamReaderUtils.readEnd(reader, FCS_NS, "DataView", true);
+
+            if (dataview != null) {
+                if (dataviews == null) {
+                    dataviews = new LinkedList<DataView>();
+                }
+                dataviews.add(dataview);
+            } else {
+                logger.warn("skipped <DataView> of type = {}", type);
+            }
+        } // while
+        return dataviews;
+    }
+
+
+    private List<Resource.ResourceFragment> parseResourceFragments(
+            XMLStreamReader reader) throws XMLStreamException,
+            SRUClientException {
+        List<Resource.ResourceFragment> resourceFragments = null;
+        while (XmlStreamReaderUtils.readStart(reader, FCS_NS, "ResourceFragment", false, true)) {
+            logger.debug("found ResourceFragment");
+            String pid = XmlStreamReaderUtils.readAttributeValue(reader, null, "pid");
+            String ref = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+            XmlStreamReaderUtils.consumeStart(reader);
+            final List<DataView> dataviews = parseDataViews(reader);
+            XmlStreamReaderUtils.readEnd(reader, FCS_NS, "ResourceFragment", true);
+
+            if (resourceFragments == null) {
+                resourceFragments = new LinkedList<Resource.ResourceFragment>();
+            }
+            resourceFragments.add(new Resource.ResourceFragment(pid, ref, dataviews));
+        } // while
+        return resourceFragments;
+    }
+
+} // class ClarinFCSRecordParser
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataView.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataView.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataView.java	(revision 2998)
@@ -0,0 +1,100 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+/**
+ * Base class for DataView implementations according to the CLARIN-FCS record
+ * schema.
+ */
+public abstract class DataView {
+    private final String type;
+    private final String pid;
+    private final String ref;
+
+
+    /**
+     * Constructor.
+     *
+     * @param type
+     *            the MIME type of this DataView
+     * @param pid
+     *            a persistent identifier or <code>null</code>
+     * @param ref
+     *            a reference URI or <code>null</code>
+     * @throws NullPointerException
+     *             if a mandatory argument was not supplied
+     *
+     */
+    protected DataView(String type, String pid, String ref) {
+        if (type == null) {
+            throw new NullPointerException("type == null");
+        }
+        this.type = type;
+        this.pid  = ((pid != null) && !pid.isEmpty()) ? pid : null;
+        this.ref  = ((ref != null) && !ref.isEmpty()) ? ref : null;
+    }
+
+
+    /**
+     * Get the MIME type of this DataView.
+     *
+     * @return the MIME type of this DataView
+     */
+    public String getMimeType() {
+        return type;
+    }
+
+
+    /**
+     * Convenience method to check if this DataView is of a certain MIME type.
+     *
+     * @param type
+     *            the MIME type to test against
+     * @return <code>true</code> if the DataView is in the supplied MIME type,
+     *         <code>false</code> otherwise
+     * @throws NullPointerException
+     *             if any required arguments are not supplied
+     */
+    public boolean isMimeType(String type) {
+        if (type == null) {
+            throw new NullPointerException("mimetype == null");
+        }
+        return (this.type.equals(type));
+    }
+
+    /**
+     * Get the persistent identifier for this DataView.
+     *
+     * @return a persistent identifier or <code>null</code> of this DataView has
+     *         none
+     */
+    public String getPid() {
+        return pid;
+    }
+
+
+    /**
+     * Get the reference URI for this DataView.
+     *
+     * @return a reference URI or <code>null</code> of this DataView has
+     *         none
+     */
+    public String getRef() {
+        return ref;
+    }
+
+} // abstract class DataView
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericDOM.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericDOM.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericDOM.java	(revision 2998)
@@ -0,0 +1,45 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import org.w3c.dom.Document;
+
+
+/**
+ * A generic DataView implementation that stores the content as a DOM document.
+ */
+public class DataViewGenericDOM extends DataView {
+    private final Document document;
+
+
+    protected DataViewGenericDOM(String type, String pid, String ref,
+            Document document) {
+        super(type, pid, ref);
+        this.document = document;
+    }
+
+
+    /**
+     * Get the DataView content.
+     * 
+     * @return the DataView content as DOM document.
+     */
+    public Document getDocument() {
+        return document;
+    }
+
+} // class GenericDataView
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericString.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericString.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericString.java	(revision 2998)
@@ -0,0 +1,42 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+/**
+ * A generic DataView implementation that stores the content as a String.
+ */
+public class DataViewGenericString extends DataView {
+    private final String content;
+
+
+    protected DataViewGenericString(String type, String pid, String ref,
+            String content) {
+        super(type, pid, ref);
+        this.content = content;
+    }
+
+
+    /**
+     * Get DataView content.
+     * 
+     * @return the DataView content as String.
+     */
+    public String getContent() {
+        return content;
+    }
+
+} // class DataViewGenericString
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewKWIC.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewKWIC.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewKWIC.java	(revision 2998)
@@ -0,0 +1,84 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+/**
+ * A CLARIN-FCS KWIC DataView.
+ */
+public final class DataViewKWIC extends DataView {
+    /**
+     * The MIME type for CLARIN-FCS KWIC data views.
+     */
+    public static final String TYPE = "application/x-clarin-fcs-kwic+xml";
+    private final String left;
+    private final String keyword;
+    private final String right;
+
+
+    /**
+     * Constructor.
+     *
+     * @param pid
+     *            a persistent identifier or <code>null</code>
+     * @param ref
+     *            a reference URI or <code>null</code>
+     * @param left
+     *            the left KWIC context
+     * @param keyword
+     *            the matched KWIC context
+     * @param right
+     *            the right KWIC context
+     */
+    DataViewKWIC(String pid, String ref, String left, String keyword,
+            String right) {
+        super(TYPE, pid, ref);
+        this.left    = (left    != null) ? left    : "";
+        this.keyword = (keyword != null) ? keyword : "";
+        this.right   = (right   != null) ? right   : "";
+    }
+
+
+    /**
+     * Get the left KWIC context.
+     *
+     * @return the left KWIC context
+     */
+    public String getLeft() {
+        return left;
+    }
+
+
+    /**
+     * Get the matched KWIC context.
+     *
+     * @return the matched KWIC context
+     */
+    public String getKeyword() {
+        return keyword;
+    }
+
+
+    /**
+     * Get the right KWIC context.
+     *
+     * @return the right KWIC context
+     */
+    public String getRight() {
+        return right;
+    }
+
+} // class DataViewKWIC
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParser.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParser.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParser.java	(revision 2998)
@@ -0,0 +1,74 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import eu.clarin.sru.client.SRUClientException;
+
+
+/**
+ * Base class for implementing parsers for parsing a specific DataView in a
+ * CLARIN-FCS record.
+ * <p>If multiple record parsers support a certain type, the one
+ * with the highest priority is selected.</p>
+ */
+public interface DataViewParser {
+
+    /**
+     * Check, if parser accepts a certain DataView type
+     *
+     * @param type
+     *            the type to be checked
+     * @return <code>true</code> if the parser supports this type,
+     *         <code>false</code> otherwise
+     */
+    public boolean acceptType(String type);
+
+
+    /**
+     * Get the priority for this parser
+     *
+     * @return the priority for this parser
+     */
+    public int getPriority();
+
+
+    /**
+     * Parse a DataView. Implementations of this methods are required to be
+     * thread-safe!
+     *
+     * @param reader
+     *            the {@link XMLStreamReader} to read from
+     * @param type
+     *            the type of the DataView
+     * @param pid
+     *            the pid of this DataView
+     * @param ref
+     *            the reference of this DataView
+     * @return the parsed {@link DataView} object
+     * @throws XMLStreamException
+     *             an error occurred while parsing the DataView
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see DataView
+     */
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException;
+
+} // interface DataViewParser
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericDOM.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericDOM.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericDOM.java	(revision 2998)
@@ -0,0 +1,128 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.stax.StAXSource;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+import eu.clarin.sru.client.SRUClientException;
+
+
+public class DataViewParserGenericDOM implements DataViewParser {
+    private static class TransformHelper {
+        private final DocumentBuilder builder;
+        private final Transformer transformer;
+
+
+        private TransformHelper(DocumentBuilder builder,
+                Transformer transformer) {
+            if (builder == null) {
+                throw new NullPointerException("builder == null");
+            }
+            this.builder = builder;
+            if (transformer == null) {
+                throw new NullPointerException("transformer == null");
+            }
+            this.transformer = transformer;
+        }
+
+
+        private Document transform(XMLStreamReader reader)
+                throws XMLStreamException, TransformerException {
+            // parse STAX to DOM fragment
+            Document document = builder.newDocument();
+            DOMResult result = new DOMResult(document);
+            transformer.transform(new StAXSource(reader), result);
+            return document;
+        }
+
+
+        private void reset() {
+            builder.reset();
+            transformer.reset();
+        }
+    } // private class TransformHelper
+    private final ThreadLocal<TransformHelper> transformHelper;
+
+    public DataViewParserGenericDOM() {
+        final DocumentBuilderFactory builderFactory =
+                DocumentBuilderFactory.newInstance();
+        final TransformerFactory transformerFactory =
+                TransformerFactory.newInstance();
+        this.transformHelper = new ThreadLocal<TransformHelper>() {
+            @Override
+            protected TransformHelper initialValue() {
+                try {
+                    return new TransformHelper(builderFactory.newDocumentBuilder(),
+                                   transformerFactory.newTransformer());
+                } catch (TransformerConfigurationException e) {
+                    throw new InternalError("unexpected error creating new transformer");
+                } catch (ParserConfigurationException e) {
+                    throw new InternalError("unexpected error creating new document builder");
+                }
+            }
+        };
+
+
+    }
+
+
+    @Override
+    public boolean acceptType(String type) {
+        return true;
+    }
+
+
+    @Override
+    public int getPriority() {
+        return Integer.MIN_VALUE;
+    }
+
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        final TransformHelper helper = transformHelper.get();
+        try {
+            final Document document = helper.transform(reader);
+            final NodeList children = document.getChildNodes();
+            if ((children != null) && (children.getLength() > 0)) {
+                return new DataViewGenericDOM(type, pid, ref, document);
+            } else {
+                throw new SRUClientException("element <DataView> does not "
+                        + "contain any nested elements");
+            }
+        } catch (TransformerException e) {
+            throw new SRUClientException("error while parsing dataview", e);
+        } finally {
+            helper.reset();
+        }
+    }
+
+} // class DataViewParserGenericDOM
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericString.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericString.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericString.java	(revision 2998)
@@ -0,0 +1,184 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.io.StringWriter;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import eu.clarin.sru.client.SRUClientException;
+
+
+public class DataViewParserGenericString implements DataViewParser {
+    private static final XMLOutputFactory factory =
+            XMLOutputFactory.newInstance();
+
+    @Override
+    public boolean acceptType(String type) {
+        return true;
+    }
+
+
+    @Override
+    public int getPriority() {
+        return Integer.MIN_VALUE;
+    }
+
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        XMLStreamWriter to = null;
+        try {
+            final StringWriter writer = new StringWriter();
+            to = factory.createXMLStreamWriter(writer);
+            copy(reader, to);
+            to.close();
+            return new DataViewGenericString(type, pid, ref, writer.toString());
+        } finally {
+            if (to != null) {
+                try {
+                    to.close();
+                } catch (XMLStreamException e) {
+                    /* IGNORE */
+                }
+            }
+        }
+    }
+
+
+    public static void copy(XMLStreamReader from, XMLStreamWriter to)
+            throws XMLStreamException {
+        int depth = 0;
+        while (from.hasNext()) {
+            depth = copyEvent(from, to, depth);
+            from.next();
+            if (depth <= 0) {
+                break;
+            }
+        } // while
+    }
+
+
+    private static int copyEvent(XMLStreamReader from, XMLStreamWriter to,
+            int depth) throws XMLStreamException {
+        switch (from.getEventType()) {
+        case XMLStreamConstants.START_DOCUMENT:
+            {
+                final String version = from.getVersion();
+                if ((version == null) || version.isEmpty()) {
+                    to.writeStartDocument();
+                } else {
+                    to.writeStartDocument(from.getCharacterEncodingScheme(),
+                            from.getVersion());
+                }
+                to.writeCharacters("\n");
+            }
+            return depth + 1;
+
+        case XMLStreamConstants.END_DOCUMENT:
+            to.writeCharacters("\n");
+            to.writeEndDocument();
+            return depth - 1;
+
+        case XMLStreamConstants.START_ELEMENT:
+            copyStartElement(from, to);
+            return depth + 1;
+
+        case XMLStreamConstants.END_ELEMENT:
+            to.writeEndElement();
+            return depth - 1;
+
+        case XMLStreamConstants.SPACE:
+            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
+                    from.getTextLength());
+            return depth;
+
+        case XMLStreamConstants.CDATA:
+            to.writeCData(from.getText());
+            return depth;
+
+        case XMLStreamConstants.CHARACTERS:
+            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
+                    from.getTextLength());
+            return depth;
+
+        case XMLStreamConstants.COMMENT:
+            to.writeComment(from.getText());
+            return depth;
+
+        case XMLStreamConstants.PROCESSING_INSTRUCTION:
+            to.writeProcessingInstruction(from.getPITarget(), from.getPIData());
+            return depth;
+
+        case XMLStreamConstants.DTD:
+        case XMLStreamConstants.ENTITY_REFERENCE:
+        case XMLStreamConstants.ATTRIBUTE:
+        case XMLStreamConstants.NAMESPACE:
+        case XMLStreamConstants.ENTITY_DECLARATION:
+        case XMLStreamConstants.NOTATION_DECLARATION:
+            /* FALL_TROUGH */
+        } // switch
+        throw new XMLStreamException("unsupported event type: " +
+                from.getEventType());
+    }
+
+    private static void copyStartElement(XMLStreamReader from,
+            XMLStreamWriter to) throws XMLStreamException {
+        final int nsCount = from.getNamespaceCount();
+        if (nsCount > 0) { // yup, got some...
+            for (int i = 0; i < nsCount; ++i) {
+                final String prefix = from.getNamespacePrefix(i);
+                final String uri = from.getNamespaceURI(i);
+                if ((prefix == null) || prefix.isEmpty()) { // default NS
+                    to.setDefaultNamespace(uri);
+                } else {
+                    to.setPrefix(prefix, uri);
+                }
+            }
+        }
+        to.writeStartElement(from.getPrefix(), from.getLocalName(),
+                from.getNamespaceURI());
+
+        if (nsCount > 0) {
+            // write namespace declarations
+            for (int i = 0; i < nsCount; ++i) {
+                final String prefix = from.getNamespacePrefix(i);
+                final String uri = from.getNamespaceURI(i);
+
+                if ((prefix == null) || prefix.isEmpty()) { // default NS
+                    to.writeDefaultNamespace(uri);
+                } else {
+                    to.writeNamespace(prefix, uri);
+                }
+            }
+        }
+        final int attrCount = from.getAttributeCount();
+        if (attrCount > 0) {
+            for (int i = 0; i < attrCount; ++i) {
+                to.writeAttribute(from.getAttributePrefix(i),
+                        from.getAttributeNamespace(i),
+                        from.getAttributeLocalName(i),
+                        from.getAttributeValue(i));
+            }
+        }
+    }
+
+} // class DataViewParserGenericString
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserKWIC.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserKWIC.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/DataViewParserKWIC.java	(revision 2998)
@@ -0,0 +1,72 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+
+public class DataViewParserKWIC implements DataViewParser {
+    private static final String FCS_KWIC_NS = "http://clarin.eu/fcs/1.0/kwic";
+    private static final String KWIC_LEGACY_TYPE = "kwic";
+    private static final Logger logger =
+            LoggerFactory.getLogger(DataViewParserKWIC.class);
+
+    @Override
+    public boolean acceptType(String type) {
+        return DataViewKWIC.TYPE.equals(type) ||
+                KWIC_LEGACY_TYPE.equals(type);
+    }
+
+    @Override
+    public int getPriority() {
+        return 1000;
+    }
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        if (KWIC_LEGACY_TYPE.equals(type)) {
+            logger.warn("type '" + KWIC_LEGACY_TYPE + "' is deprecteded for a KWIC <DataView>, please use '" + DataViewKWIC.TYPE + "' instead");
+        }
+        String left = null;
+        String keyword = null;
+        String right = null;
+
+        XmlStreamReaderUtils.readStart(reader, FCS_KWIC_NS, "kwic", true);
+        if (XmlStreamReaderUtils.readStart(reader, FCS_KWIC_NS, "c", false)) {
+            left = XmlStreamReaderUtils.readString(reader, false);
+            XmlStreamReaderUtils.readEnd(reader, FCS_KWIC_NS, "c");
+        }
+        keyword = XmlStreamReaderUtils.readContent(reader, FCS_KWIC_NS, "kw", true);
+        if (XmlStreamReaderUtils.readStart(reader, FCS_KWIC_NS, "c", false)) {
+            right = XmlStreamReaderUtils.readString(reader, false);
+            XmlStreamReaderUtils.readEnd(reader, FCS_KWIC_NS, "c");
+        }
+        XmlStreamReaderUtils.readEnd(reader, FCS_KWIC_NS, "kwic");
+
+        logger.debug("left='{}' keyword='{}', right='{}'",
+                left, keyword, right);
+        return new DataViewKWIC(pid, ref, left, keyword, right);
+    }
+
+} // class DataViewParserKWIC
Index: SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/Resource.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/Resource.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/main/java/eu/clarin/sru/client/fcs/Resource.java	(revision 2998)
@@ -0,0 +1,180 @@
+/**
+ * This software is copyright (c) 2012-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A CLARIN-FCS resource
+ */
+public class Resource {
+    /**
+     * A CLARIN-FCS resource fragment
+     */
+    public final static class ResourceFragment {
+        private final String pid;
+        private final String ref;
+        private final List<DataView> dataviews;
+
+
+        ResourceFragment(String pid, String ref, List<DataView> dataviews) {
+            this.pid = ((pid != null) && !pid.isEmpty()) ? pid : null;
+            this.ref = ((ref != null) && !ref.isEmpty()) ? ref : null;
+            if ((dataviews != null) && !dataviews.isEmpty()) {
+                this.dataviews = Collections.unmodifiableList(dataviews);
+            } else {
+                this.dataviews = null;
+            }
+        }
+
+
+        /**
+         * Get the persistent identifier for this resource fragment.
+         *
+         * @return a persistent identifier or <code>null</code> of this resource
+         *         fragment has none
+         */
+        public String getPid() {
+            return pid;
+        }
+
+
+        /**
+         * Get the reference URI for this resource fragment.
+         *
+         * @return a reference URI or <code>null</code> of this resource
+         *         fragment has none
+         */
+        public String getRef() {
+            return ref;
+        }
+
+
+        /**
+         * Convenience method to check if this resource fragment has any
+         * data views.
+         *
+         * @return <code>true</code> if this resource fragment has data views,
+         *         <code>false</code> otherwise
+         */
+        public boolean hasDataViews() {
+            return (dataviews != null);
+        }
+
+
+        /**
+         * Get the list of data view objects for this this resource fragment.
+         *
+         * @return a list of {@link DataView} objects or <code>null</code>,
+         *         or <code>null</code> if this resource fragment has
+         *         none
+         */
+        public List<DataView> getDataViews() {
+            return dataviews;
+        }
+    } // inner class ResourceFragment
+    private final String pid;
+    private final String ref;
+    private final List<DataView> dataviews;
+    private final List<ResourceFragment> resourceFragments;
+
+
+    Resource(String pid, String ref, List<DataView> dataviews,
+            List<ResourceFragment> resourceFragments) {
+        this.pid = ((pid != null) && !pid.isEmpty()) ? pid : null;
+        this.ref = ((ref != null) && !ref.isEmpty()) ? ref : null;
+        if ((dataviews != null) && !dataviews.isEmpty()) {
+            this.dataviews = Collections.unmodifiableList(dataviews);
+        } else {
+            this.dataviews = null;
+        }
+        if ((resourceFragments != null) && !resourceFragments.isEmpty()) {
+            this.resourceFragments =
+                    Collections.unmodifiableList(resourceFragments);
+        } else {
+            this.resourceFragments = null;
+        }
+    }
+
+
+    /**
+     * Get the persistent identifier for this resource.
+     *
+     * @return a persistent identifier or <code>null</code> of this resource has
+     *         none
+     */
+    public String getPid() {
+        return pid;
+    }
+
+
+    /**
+     * Get the reference URI for this resource.
+     *
+     * @return a reference URI or <code>null</code> of this resource has
+     *         none
+     */
+    public String getRef() {
+        return ref;
+    }
+
+
+    /**
+     * Convenience method to check if this resource has any dataviews.
+     *
+     * @return <code>true</code> if this resource has dataviews,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasDataViews() {
+        return (dataviews != null);
+    }
+
+
+    /**
+     * Get the list of dataview objects for this this resource.
+     *
+     * @return a list of {@link DataView} objects or <code>null</code>, or
+     *         <code>null</code> if this resource has none
+     */
+    public List<DataView> getDataViews() {
+        return dataviews;
+    }
+
+
+    /**
+     * Convenience method to check if this resource has any resource fragments.
+     *
+     * @return <code>true</code> if this resource has resource fragments,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasResourceFragments() {
+        return (resourceFragments != null);
+    }
+
+
+    /**
+     * Get the list of resource fragment objects for this this resource.
+     *
+     * @return a list of {@link ResourceFragment} objects or <code>null</code>,
+     *         or <code>null</code> if this resource has none
+     */
+    public List<ResourceFragment> getResourceFragments() {
+        return resourceFragments;
+    }
+
+} // class Resource
Index: SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestClient.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestClient.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestClient.java	(revision 2998)
@@ -0,0 +1,88 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.fcs.ClarinFCSRecordParser;
+
+
+public class TestClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestClient.class);
+
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+            SRUClient client = new SRUClient();
+
+            // register record data parsers
+            client.registerRecordParser(new ClarinFCSRecordParser());
+
+            // explain
+            try {
+                logger.info("performing 'explain' request ...");
+                SRUExplainRequest request = TestUtils.makeExplainRequest(args[0]);
+                SRUExplainResponse response = client.explain(request);
+                TestUtils.printExplainResponse(response);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'explain' request", e);
+            }
+
+            // scan
+            try {
+                logger.info("performing 'scan' request ...");
+                SRUScanRequest request = TestUtils.makeScanRequest(args[0]);
+                SRUScanResponse response = client.scan(request);
+                TestUtils.printScanResponse(response);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'scan' request", e);
+            }
+
+            // searchRetrieve
+            try {
+                logger.info("performing 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request = TestUtils.makeSearchRequest(args[0], null);
+                SRUSearchRetrieveResponse response = client.searchRetrieve(request);
+                TestUtils.printSearchResponse(response);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'searchRetrieve' request", e);
+            }
+
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+    static {
+        org.apache.log4j.BasicConfigurator
+                .configure(new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger = org.apache.log4j.Logger
+                .getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin")
+                .setLevel(org.apache.log4j.Level.DEBUG);
+    }
+
+}
Index: SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestSimpleClient.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestSimpleClient.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestSimpleClient.java	(revision 2998)
@@ -0,0 +1,163 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.fcs.ClarinFCSRecordData;
+import eu.clarin.sru.client.fcs.ClarinFCSRecordParser;
+
+
+public class TestSimpleClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestSimpleClient.class);
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+            SRUSimpleClient client = new SRUSimpleClient();
+
+            // register record data parsers
+            client.registerRecordParser(new ClarinFCSRecordParser());
+
+            /*
+             * just use one dump handler for each request.
+             * A real application should be smarter here and use the
+             * appropriate handler
+             */
+            SRUDefaultHandlerAdapter handler = new SRUDefaultHandlerAdapter() {
+                @Override
+                public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+                        throws SRUClientException {
+                    for (SRUDiagnostic diagnostic : diagnostics) {
+                        logger.info("onDiagnostics: uri={}, detail={}, message={}",
+                                diagnostic.getURI(), diagnostic.getDetails(),
+                                diagnostic.getMessage());
+                    }
+                }
+
+                @Override
+                public void onRequestStatistics(int bytes, long millisTotal,
+                        long millisNetwork, long millisParsing) {
+                    logger.info("onRequestStatistics(): {} bytes in {} millis",
+                            bytes, millisTotal);
+                }
+
+                @Override
+                public void onStartTerms() throws SRUClientException {
+                    logger.info("onStartTerms()");
+                }
+
+                @Override
+                public void onFinishTerms() throws SRUClientException {
+                    logger.info("onFinishTerms()");
+                }
+
+                @Override
+                public void onTerm(String value, int numberOfRecords,
+                        String displayTerm, SRUWhereInList whereInList)
+                        throws SRUClientException {
+                    logger.info("onTerm(): value = {}, numberOfRecords = {}, " +
+                            "displayTerm = {}, whereInList = {}",
+                            value, numberOfRecords, displayTerm, whereInList);
+                }
+
+                @Override
+                public void onStartRecords(int numberOfRecords,
+                        String resultSetId, int resultSetIdleTime)
+                        throws SRUClientException {
+                    logger.info("onStartRecords(): numberOfRecords = {}",
+                            numberOfRecords);
+                }
+
+                @Override
+                public void onFinishRecords(int nextRecordPosition)
+                        throws SRUClientException {
+                    logger.info("onFinishRecords(): nextRecordPosition = {}",
+                            nextRecordPosition);
+                }
+
+                @Override
+                public void onRecord(String identifier, int position,
+                        SRURecordData data) throws SRUClientException {
+                    logger.info("onRecord(): identifier = {}, position = {}, schema = {}",
+                                identifier, position, data.getRecordSchema());
+                    if (ClarinFCSRecordData.RECORD_SCHEMA.equals(data.getRecordSchema())) {
+                        ClarinFCSRecordData record =
+                                (ClarinFCSRecordData) data;
+                        TestUtils.dumpResource(record.getResource());
+                    } else if (SRUExplainRecordData.RECORD_SCHEMA.equals(data.getRecordSchema())) {
+                        TestUtils.dumpExplainRecordData(data);
+                    }
+                }
+
+                @Override
+                public void onSurrogateRecord(String identifier, int position,
+                        SRUDiagnostic data) throws SRUClientException {
+                    logger.info("onSurrogateRecord: identifier = {}, position = {}, uri={}, detail={}, message={}",
+                            identifier, position, data.getURI(), data.getDetails(), data.getMessage());
+                }
+            };
+
+            try {
+                logger.info("performing 'explain' request ...");
+                SRUExplainRequest request = TestUtils.makeExplainRequest(args[0]);
+                client.explain(request, handler);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'explain' request", e);
+            }
+
+            try {
+                logger.info("performing 'scan' request ...");
+                SRUScanRequest request = TestUtils.makeScanRequest(args[0]);
+                client.scan(request, handler);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'scan' request", e);
+            }
+
+            try {
+                logger.info("performing 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request = TestUtils.makeSearchRequest(args[0], null);
+                client.searchRetrieve(request, handler);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'searchRetrieve' request", e);
+            }
+
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+    static {
+        org.apache.log4j.BasicConfigurator.configure(
+                new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin").setLevel(
+                org.apache.log4j.Level.DEBUG);
+    }
+
+} // class TestSimpleClient
Index: SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestThreadedClient.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestThreadedClient.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestThreadedClient.java	(revision 2998)
@@ -0,0 +1,114 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.fcs.ClarinFCSRecordParser;
+
+@Deprecated
+public class TestThreadedClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestThreadedClient.class);
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+            SRUThreadedClient client = new SRUThreadedClient();
+
+            // register record data parsers
+            client.registerRecordParser(new ClarinFCSRecordParser());
+
+            try {
+                /*
+                 * the following requests will be run asynchronously and
+                 * concurrently
+                 */
+                logger.info("submitting 'explain' request ...");
+                SRUExplainRequest request1 = TestUtils.makeExplainRequest(args[0]);
+                Future<SRUExplainResponse> result1 = client.explain(request1);
+
+                logger.info("submitting 'scan' request ...");
+                SRUScanRequest request2 = TestUtils.makeScanRequest(args[0]);
+                Future<SRUScanResponse> result2 = client.scan(request2);
+
+                logger.info("submitting 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request3 = TestUtils.makeSearchRequest(args[0], null);
+                Future<SRUSearchRetrieveResponse> result3 =
+                        client.searchRetrieve(request3);
+
+                /*
+                 * HACK: the following code is quick and dirty. Don't every busy
+                 * wait on responses like this in a real-world application!
+                 */
+                while (!(result1.isDone() && result2.isDone() &&
+                        result3.isDone())) {
+                    logger.debug("waiting for results ...");
+                    try {
+                        Thread.sleep(125);
+                    } catch (InterruptedException e) {
+                        /* IGNORE */
+                    }
+                }
+
+                try {
+                    TestUtils.printExplainResponse(result1.get());
+                } catch (ExecutionException e) {
+                    logger.error("some error occured while performing 'explain' request", e);
+                }
+                try {
+                    TestUtils.printScanResponse(result2.get());
+                } catch (ExecutionException e) {
+                    logger.error("some error occured while performing 'scan' request", e);
+                }
+                try {
+                    TestUtils.printSearchResponse(result3.get());
+                } catch (ExecutionException e) {
+                    logger.error("some error occured while performing 'searchRetrieve' request", e);
+                }
+            } catch (Exception e) {
+                logger.error("a fatal error occured while performing request", e);
+            }
+
+            client.shutdown();
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+
+
+    static {
+        org.apache.log4j.BasicConfigurator.configure(
+                new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin").setLevel(
+                org.apache.log4j.Level.DEBUG);
+    }
+
+}
Index: SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestThreadedClientCallback.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestThreadedClientCallback.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestThreadedClientCallback.java	(revision 2998)
@@ -0,0 +1,130 @@
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.fcs.ClarinFCSRecordParser;
+
+
+public class TestThreadedClientCallback {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestThreadedClientCallback.class);
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+            SRUThreadedClient client = new SRUThreadedClient();
+
+            // register record data parsers
+            client.registerRecordParser(new ClarinFCSRecordParser());
+
+            try {
+                final CountDownLatch latch = new CountDownLatch(3);
+
+                /*
+                 * the following requests will be run asynchronously and
+                 * concurrently
+                 * Invoke requests and supply a callback, that
+                 *  a) prints the results
+                 *  b) downs a latch (which is used to make the main-thread wait
+                 *     on the requests to be completed)
+                 */
+                logger.info("submitting 'explain' request ...");
+                SRUExplainRequest request1 =
+                        TestUtils.makeExplainRequest(args[0]);
+                client.explain(request1, new SRUCallback<SRUExplainRequest, SRUExplainResponse>() {
+                    @Override
+                    public void onSuccess(SRUExplainResponse response) {
+                        TestUtils.printExplainResponse(response);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(SRUExplainRequest request,
+                            SRUClientException error) {
+                        logger.error("error while performing request", error);
+                        latch.countDown();
+                    }
+                });
+
+                logger.info("submitting 'scan' request ...");
+                SRUScanRequest request2 =
+                        TestUtils.makeScanRequest(args[0]);
+                client.scan(request2, new SRUCallback<SRUScanRequest, SRUScanResponse>() {
+                    @Override
+                    public void onSuccess(SRUScanResponse response) {
+                        TestUtils.printScanResponse(response);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(SRUScanRequest request,
+                            SRUClientException error) {
+                        logger.error("error while performing request", error);
+                        latch.countDown();
+                    }
+                });
+
+                logger.info("submitting 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request3 =
+                        TestUtils.makeSearchRequest(args[0], null);
+                client.searchRetrieve(request3, new SRUCallback<SRUSearchRetrieveRequest, SRUSearchRetrieveResponse>() {
+                    @Override
+                    public void onSuccess(SRUSearchRetrieveResponse response) {
+                        TestUtils.printSearchResponse(response);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(SRUSearchRetrieveRequest request,
+                            SRUClientException error) {
+                        logger.error("error while performing request", error);
+                        latch.countDown();
+                    }
+                });
+
+                latch.await();
+            } catch (Exception e) {
+                logger.error("a fatal error occured while performing request", e);
+            }
+
+            client.shutdown();
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+    static {
+        org.apache.log4j.BasicConfigurator.configure(
+                new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin").setLevel(
+                org.apache.log4j.Level.DEBUG);
+    }
+
+}
Index: SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestUtils.java
===================================================================
--- SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestUtils.java	(revision 2998)
+++ SRUClient/tags/SRUClient-0.9.2/src/test/java/eu/clarin/sru/client/TestUtils.java	(revision 2998)
@@ -0,0 +1,230 @@
+/**
+/**
+ * This software is copyright (c) 2011-2013 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Node;
+
+import eu.clarin.sru.client.SRUExplainRecordData.ConfigInfo;
+import eu.clarin.sru.client.SRUExplainRecordData.Schema;
+import eu.clarin.sru.client.fcs.ClarinFCSRecordData;
+import eu.clarin.sru.client.fcs.DataView;
+import eu.clarin.sru.client.fcs.DataViewGenericDOM;
+import eu.clarin.sru.client.fcs.DataViewGenericString;
+import eu.clarin.sru.client.fcs.DataViewKWIC;
+import eu.clarin.sru.client.fcs.Resource;
+
+class TestUtils {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestUtils.class);
+
+    public static SRUExplainRequest makeExplainRequest(String baseURI) {
+        SRUExplainRequest request = new SRUExplainRequest(baseURI);
+        request.setExtraRequestData("x-indent-response", "4");
+        request.setParseRecordDataEnabled(true);
+        return request;
+    }
+
+
+    public static SRUScanRequest makeScanRequest(String baseURI) {
+        SRUScanRequest request = new SRUScanRequest(baseURI);
+        request.setScanClause("fcs.resource = root");
+        request.setExtraRequestData("x-clarin-resource-info", "true");
+        request.setExtraRequestData("x-indent-response", "4");
+        return request;
+    }
+
+
+    public static SRUSearchRetrieveRequest makeSearchRequest(String baseURI, String query) {
+        if ((query == null) || query.isEmpty()) {
+            query = "Faustus";
+        }
+        SRUSearchRetrieveRequest request = new SRUSearchRetrieveRequest(baseURI);
+        request.setQuery(query);
+        request.setRecordSchema(ClarinFCSRecordData.RECORD_SCHEMA);
+        request.setMaximumRecords(5);
+        request.setRecordPacking(SRURecordPacking.XML);
+        request.setExtraRequestData("x-indent-response", "4");
+        return request;
+    }
+
+
+    public static void printExplainResponse(SRUExplainResponse response) {
+        logger.info("displaying results of 'explain' request ...");
+        if (response.hasDiagnostics()) {
+            for (SRUDiagnostic diagnostic : response.getDiagnostics()) {
+                logger.info("uri={}, message={}, detail={}",
+                        diagnostic.getURI(),
+                        diagnostic.getMessage(),
+                        diagnostic.getDetails());
+            }
+        }
+        if (response.hasRecord()) {
+            SRURecord record = response.getRecord();
+            logger.info("schema = {}", record.getRecordSchema());
+            if (record.isRecordSchema(SRUExplainRecordData.RECORD_SCHEMA)) {
+                dumpExplainRecordData(record.getRecordData());
+            }
+        }
+    }
+
+
+    public static void printScanResponse(SRUScanResponse response) {
+        logger.info("displaying results of 'scan' request ...");
+        if (response.hasDiagnostics()) {
+            for (SRUDiagnostic diagnostic : response.getDiagnostics()) {
+                logger.info("uri={}, message={}, detail={}",
+                        diagnostic.getURI(),
+                        diagnostic.getMessage(),
+                        diagnostic.getDetails());
+            }
+        }
+        if (response.hasTerms()) {
+            for (SRUTerm term : response.getTerms()) {
+                logger.info("value={}, numberOfRecords={}, displayTerm={}",
+                            term.getValue(),
+                            term.getNumberOfRecords(),
+                            term.getDisplayTerm());
+            }
+        } else {
+            logger.info("no terms");
+        }
+    }
+
+
+    public static void printSearchResponse(SRUSearchRetrieveResponse response) {
+        logger.info("displaying results of 'searchRetrieve' request ...");
+        logger.info("numberOfRecords = {}, nextResultPosition = {}",
+                response.getNumberOfRecords(),
+                response.getNextRecordPosition());
+        if (response.hasDiagnostics()) {
+            for (SRUDiagnostic diagnostic : response.getDiagnostics()) {
+                logger.info("uri={}, message={}, detail={}",
+                        diagnostic.getURI(),
+                        diagnostic.getMessage(),
+                        diagnostic.getDetails());
+            }
+        }
+        if (response.hasRecords()) {
+            for (SRURecord record : response.getRecords()) {
+                logger.info("schema = {}, identifier = {}, position = {}",
+                        record.getRecordSchema(),
+                        record.getRecordIdentifier(),
+                        record.getRecordPosition());
+                if (record.isRecordSchema(ClarinFCSRecordData.RECORD_SCHEMA)) {
+                    ClarinFCSRecordData rd =
+                            (ClarinFCSRecordData) record.getRecordData();
+                    dumpResource(rd.getResource());
+                } else if (record.isRecordSchema(SRUSurrogateRecordData.RECORD_SCHEMA)) {
+                    SRUSurrogateRecordData r =
+                            (SRUSurrogateRecordData) record.getRecordData();
+                    logger.info("SURROGATE DIAGNOSTIC: uri={}, message={}, detail={}",
+                                r.getURI(), r.getMessage(), r.getDetails());
+                } else {
+                    logger.info("UNSUPPORTED SCHEMA: {}",
+                            record.getRecordSchema());
+                }
+            }
+        } else {
+            logger.info("no results");
+        }
+    }
+
+
+    public static void dumpExplainRecordData(SRURecordData recordData) {
+        if (SRUExplainRecordData.RECORD_SCHEMA.equals(recordData.getRecordSchema())) {
+            SRUExplainRecordData data = (SRUExplainRecordData) recordData;
+            logger.info("host={}, port={}, database={}",
+                    data.getServerInfo().getHost(),
+                    data.getServerInfo().getPort(),
+                    data.getServerInfo().getDatabase());
+            List<Schema> schemaInfo = data.getSchemaInfo();
+            if (schemaInfo != null) {
+                for (Schema schema : schemaInfo) {
+                    logger.debug("schema: identifier={}, name={}, " +
+                            "location={}, sort={}, retrieve={}",
+                            schema.getIdentifier(),
+                            schema.getName(),
+                            schema.getLocation(),
+                            schema.getSort(),
+                            schema.getRetrieve());
+                }
+            }
+            ConfigInfo configInfo = data.getConfigInfo();
+            if (configInfo != null) {
+                if (configInfo.getDefaults() != null) {
+                    logger.debug("configInfo/default = {}",
+                            configInfo.getDefaults());
+                }
+                if (configInfo.getSettings() != null) {
+                    logger.debug("configInfo/setting = {}",
+                            configInfo.getSettings());
+                }
+                if (configInfo.getSupports() != null) {
+                    logger.debug("configInfo/supports = {}",
+                            configInfo.getSupports());
+                }
+            }
+        }
+    }
+
+
+    public static void dumpResource(Resource resource) {
+        logger.info("CLARIN-FCS: pid={}, ref={}",
+                resource.getPid(), resource.getRef());
+        if (resource.hasDataViews()) {
+            dumpDataView("CLARIN-FCS: ", resource.getDataViews());
+        }
+        if (resource.hasResourceFragments()) {
+            for (Resource.ResourceFragment fragment : resource.getResourceFragments()) {
+                logger.debug("CLARIN-FCS: ResourceFragment: pid={}, ref={}",
+                        fragment.getPid(), fragment.getRef());
+                if (fragment.hasDataViews()) {
+                    dumpDataView("CLARIN-FCS: ResourceFragment/", fragment.getDataViews());
+                }
+            }
+        }
+    }
+
+
+    private static void dumpDataView(String s, List<DataView> dataviews) {
+        for (DataView dataview : dataviews) {
+            logger.info("{}DataView: type={}, pid={}, ref={}",
+                    s, dataview.getMimeType(), dataview.getPid(),
+                    dataview.getRef());
+            if (dataview instanceof DataViewGenericDOM) {
+                final DataViewGenericDOM view = (DataViewGenericDOM) dataview;
+                final Node root = view.getDocument().getFirstChild();
+                logger.info("{}DataView: root element <{}> / {}",
+                        s, root.getNodeName(),
+                        root.getOwnerDocument().hashCode());
+            } else if (dataview instanceof DataViewGenericString) {
+                final DataViewGenericString view = (DataViewGenericString) dataview;
+                logger.info("{}DataView: data = {}", s, view.getContent());
+            } else if (dataview.isMimeType(DataViewKWIC.TYPE)) {
+                final DataViewKWIC kw = (DataViewKWIC) dataview;
+                logger.info("{}DataView: {} / {} / {}",
+                        s, kw.getLeft(), kw.getKeyword(), kw.getRight());
+            }
+        }
+    }
+
+} // class TestUtils
