From 5265c92c5518ba2db870d9ca24562ac017d2057a Mon Sep 17 00:00:00 2001 From: David Maus <maus@hab.de> Date: Thu, 7 Feb 2013 11:24:17 +0100 Subject: [PATCH] Import VuFindSearch module --- module/VuFindSearch/LICENSE | 278 ++++++++ module/VuFindSearch/Module.php | 110 +++ module/VuFindSearch/config/.keep | 0 .../VuFindSearch/Backend/BackendInterface.php | 103 +++ .../Backend/Exception/BackendException.php | 44 ++ .../Exception/RemoteErrorException.php | 45 ++ .../Exception/RequestErrorException.php | 45 ++ .../src/VuFindSearch/Backend/Solr/Backend.php | 350 ++++++++++ .../VuFindSearch/Backend/Solr/Connector.php | 459 ++++++++++++ .../Backend/Solr/QueryBuilder.php | 653 ++++++++++++++++++ .../Backend/Solr/Response/Json/Record.php | 108 +++ .../Solr/Response/Json/RecordCollection.php | 243 +++++++ .../Response/Json/RecordCollectionFactory.php | 93 +++ .../Backend/Solr/SearchHandler.php | 448 ++++++++++++ .../Exception/ExceptionInterface.php | 43 ++ .../Exception/InvalidArgumentException.php | 42 ++ .../Exception/RuntimeException.php | 43 ++ .../src/VuFindSearch/ParamBag.php | 199 ++++++ .../src/VuFindSearch/Query/AbstractQuery.php | 42 ++ .../src/VuFindSearch/Query/Params.php | 269 ++++++++ .../src/VuFindSearch/Query/Query.php | 120 ++++ .../src/VuFindSearch/Query/QueryAdapter.php | 71 ++ .../src/VuFindSearch/Query/QueryGroup.php | 210 ++++++ .../RecordCollectionFactoryInterface.php | 8 + .../Response/RecordCollectionInterface.php | 115 +++ .../VuFindSearch/Response/RecordInterface.php | 62 ++ .../VuFindSearch/src/VuFindSearch/Service.php | 296 ++++++++ .../tests/unit-tests/bootstrap.php | 24 + .../unit-tests/fixtures/searchspecs.yaml | 495 +++++++++++++ .../fixtures/solr/response/bad-request | 1 + .../solr/response/internal-server-error | 1 + .../fixtures/solr/response/no-match | 6 + .../fixtures/solr/response/single-record | 6 + .../VuFindSearch/tests/unit-tests/phpunit.xml | 12 + .../VuFindSearch/tests/unit-tests/src/.keep | 0 .../VuFindTest/Backend/Solr/BackendTest.php | 88 +++ .../VuFindTest/Backend/Solr/ConnectorTest.php | 143 ++++ .../Backend/Solr/QueryBuilderTest.php | 148 ++++ .../Json/RecordCollectionFactoryTest.php | 58 ++ .../Backend/Solr/SearchHandlerTest.php | 57 ++ 40 files changed, 5538 insertions(+) create mode 100644 module/VuFindSearch/LICENSE create mode 100644 module/VuFindSearch/Module.php create mode 100644 module/VuFindSearch/config/.keep create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php create mode 100644 module/VuFindSearch/src/VuFindSearch/ParamBag.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/Params.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/Query.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php create mode 100644 module/VuFindSearch/src/VuFindSearch/Service.php create mode 100644 module/VuFindSearch/tests/unit-tests/bootstrap.php create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record create mode 100644 module/VuFindSearch/tests/unit-tests/phpunit.xml create mode 100644 module/VuFindSearch/tests/unit-tests/src/.keep create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php diff --git a/module/VuFindSearch/LICENSE b/module/VuFindSearch/LICENSE new file mode 100644 index 00000000000..4eb3f1e9596 --- /dev/null +++ b/module/VuFindSearch/LICENSE @@ -0,0 +1,278 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/module/VuFindSearch/Module.php b/module/VuFindSearch/Module.php new file mode 100644 index 00000000000..f2406e40d2f --- /dev/null +++ b/module/VuFindSearch/Module.php @@ -0,0 +1,110 @@ +<?php + +/** + * ZF2 module definition for the VF2 search service. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch; + +use Zend\ServiceManager\ServiceManager; +use Zend\EventManager\EventManager; + +/** + * ZF2 module definition for the VF2 search service. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class Module +{ + + /** + * Return autoloader configuration. + * + * @return array + */ + public function getAutoloaderConfig () + { + return array( + 'Zend\Loader\StandardAutoloader' => array( + 'namespaces' => array( + __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, + ), + ), + ); + } + + /** + * Return service configuration. + * + * @return array + */ + public function getServiceConfig () + { + return array( + 'factories' => array( + 'VuFind\Search' => array($this, 'setup'), + ) + ); + } + + /** + * Return configured search service to superior service manager. + * + * @param ServiceManager $sm Service manager + * + * @return SearchService + */ + public function setup (ServiceManager $sm) + { + $service = new Service(); + if ($sm->has('VuFind\Logger')) { + $service->setLogger($sm->get('VuFind\Logger')); + } + return $service; + } + +} \ No newline at end of file diff --git a/module/VuFindSearch/config/.keep b/module/VuFindSearch/config/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php b/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php new file mode 100644 index 00000000000..2ee42f57844 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php @@ -0,0 +1,103 @@ +<?php + +/** + * Search backend interface definition. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend; + +use VuFindSearch\Query\AbstractQuery; +use VuFindSearch\Query\QueryGroup; +use VuFindSearch\Query\Query; + +use VuFindSearch\Query\Params; + +/** + * Search backend interface definition. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +interface BackendInterface +{ + + /** + * Perform a search and return record collection. + * + * @param AbstractQuery $query Search query + * @param Params $params Search parameters + * + * @return RecordCollectionInterface + */ + public function search (AbstractQuery $query, Params $params); + + /** + * Retrieve a single document. + * + * @param string $id Document identifier + * + * @return RecordCollectionInterface + */ + public function retrieve ($id); + + /** + * Delete a single record. + * + * @param string $id Record identifier + * + * @return RecordCollectionInterface + */ + public function delete ($id); + + /** + * Return similar records. + * + * @param string $id Id of record to compare with + * + * @return RecordCollectionInterface + */ + public function similar ($id); +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php new file mode 100644 index 00000000000..5b8ecfe185a --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php @@ -0,0 +1,44 @@ +<?php + +/** + * BackendException. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Exception; + +use VuFindSearch\Exception\RuntimeException; + +/** + * BackendException. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class BackendException extends RuntimeException +{} diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php new file mode 100644 index 00000000000..acc7755809b --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php @@ -0,0 +1,45 @@ +<?php + +/** + * RemoteErrorException. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Exception; + +/** + * RemoteErrorException. + * + * Signal an exceptional error of the remote backend service, e.g. indicated + * by a 5xx response code. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class RemoteErrorException extends BackendException +{} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php new file mode 100644 index 00000000000..0ddc78194c4 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php @@ -0,0 +1,45 @@ +<?php + +/** + * RequestErrorException. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Exception; + +/** + * RequestErrorException. + * + * Signals an error in the request to the remote service, e.g. indicated by a + * 4xx response code. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class RequestErrorException extends BackendException +{} diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php new file mode 100644 index 00000000000..566c1fd317b --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php @@ -0,0 +1,350 @@ +<?php + +/** + * SOLR backend. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr; + +use VuFindSearch\Query\AbstractQuery; +use VuFindSearch\Query\QueryGroup; +use VuFindSearch\Query\Query; +use VuFindSearch\Query\Params; + +use VuFindSearch\Response\RecordCollectionInterface; +use VuFindSearch\Response\RecordCollectionFactoryInterface; + +use VuFindSearch\Backend\BackendInterface; + +use Zend\Log\LoggerInterface; + +use VuFindSearch\Backend\Exception\BackendException; + +/** + * SOLR backend. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class Backend implements BackendInterface +{ + /** + * Record collection factory. + * + * @var RecordCollectionFactoryInterface + */ + protected $collectionFactory; + + /** + * Dictionaries for spellcheck. + * + * @var array + */ + protected $dictionaries; + + /** + * Logger, if any. + * + * @var LoggerInterface + */ + protected $logger; + + /** + * Connector. + * + * @var Connector + */ + protected $connector; + + /** + * Backend identifier. + * + * @var string + */ + protected $identifier; + + /** + * Query builder. + * + * @var QueryBuilder + */ + protected $queryBuilder; + + /** + * Constructor. + * + * @param string $identifier Backend identifier + * @param Connector $connector SOLR connector + * + * @return void + */ + public function __construct ($identifier, Connector $connector) + { + $this->connector = $connector; + $this->identifier = $identifier; + $this->dictionaries = array(); + } + + /** + * Set the spellcheck dictionaries to use. + * + * @param array $dictionaries Spellcheck dictionaries + * + * @return void + */ + public function setDictionaries (array $dictionaries) + { + $this->dictionaries = $dictionaries; + } + + /** + * Perform a search and return record collection. + * + * @param AbstractQuery $query Search query + * @param Params $params Search parameters + * + * @return RecordCollectionInterface + * + * @todo Disable more SOLR request options when resubmitting for spellcheck + * @todo Implement merge of spellcheck results + */ + public function search (AbstractQuery $query, Params $params) + { + if ($params->isSpellcheckEnabled()) { + if (!empty($this->dictionaries)) { + reset($this->dictionaries); + $params->setSpellcheckDictionary(current($this->dictionaries)); + } else { + $this->log('warn', 'Spellcheck requested but no spellcheck dictionary configured'); + } + } + + $response = $this->connector->search($query, $params, $this->getQueryBuilder()); + $collection = $this->getRecordCollectionFactory()->factory($this->deserialize($response)); + $this->injectSourceIdentifier($collection); + + // Submit requests for more spelling suggestions + while (next($this->dictionaries) !== false) { + $req = $this->connector->getLastRequestParameters(); + $req->set('spellcheck.dictionary', array(current($this->dictionaries))); + $req->set('rows', array(0)); + $response = $this->connector->resubmit(); + } + + return $collection; + } + + /** + * Retrieve a single document. + * + * @param string $id Document identifier + * + * @return RecordCollectionInterface + */ + public function retrieve ($id) + { + $response = $this->connector->retrieve($id); + $collection = $this->getRecordCollectionFactory()->factory($this->deserialize($response)); + $this->injectSourceIdentifier($collection); + return $collection; + } + + /** + * Return similar records. + * + * @param string $id Id of record to compare with + * + * @return RecordCollectionInterface + */ + public function similar ($id) + { + $response = $this->connector->similar($id); + $collection = $this->getRecordCollectionFactory()->factory($this->deserialize($response)); + $this->injectSourceIdentifier($collection); + return $collection; + } + + /** + * Delete a single record. + * + * @param string $id Record identifier + * + * @return void + * + * @todo Currently not implemented in the connector + */ + public function delete ($id) + { + $this->connector->delete($id); + } + + /** + * Set the Logger. + * + * @param LoggerInterface $logger Logger + * + * @return void + */ + public function setLogger (LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Return query builder. + * + * Lazy loads an empty QueryBuilder if none was set. + * + * @return QueryBuilder + */ + public function getQueryBuilder () + { + if (!$this->queryBuilder) { + $this->queryBuilder = new QueryBuilder(); + } + return $this->queryBuilder; + } + + /** + * Set the query builder. + * + * @param QueryBuilder $queryBuilder Query builder + * + * @return void + * + * @todo Typehint QueryBuilderInterface + */ + public function setQueryBuilder (QueryBuilder $queryBuilder) + { + $this->queryBuilder = $queryBuilder; + } + + /** + * Return backend identifier. + * + * @return string + */ + public function getIdentifier () + { + return $this->identifier; + } + + /** + * Set the record collection factory. + * + * @param RecordCollectionFactoryInterface $factory Factory + * + * @return void + */ + public function setRecordCollectionFactory (RecordCollectionFactoryInterface $factory) + { + $this->collectionFactory = $factory; + } + + /** + * Return the record collection factory. + * + * Lazy loads a generic collection factory. + * + * @return RecordCollectionFactoryInterface + */ + public function getRecordCollectionFactory () + { + if (!$this->collectionFactory) { + $this->collectionFactory = new Response\Json\RecordCollectionFactory(); + } + return $this->collectionFactory; + } + + /** + * Return the SOLR connector. + * + * @return ConnectorInterface + */ + public function getConnector () + { + return $this->connector; + } + + /// Internal API + + /** + * Inject source identifier in record collection and all contained records. + * + * @param ResponseInterface $response Response + * + * @return void + */ + protected function injectSourceIdentifier (RecordCollectionInterface $response) + { + $response->setSourceIdentifier($this->identifier); + foreach ($response as $record) { + $record->setSourceIdentifier($this->identifier); + } + return $response; + } + + /** + * Send a message to the logger. + * + * @param string $level Log level + * @param string $message Log message + * @param array $context Log context + * + * @return void + */ + protected function log ($level, $message, array $context = array()) + { + if ($this->logger) { + $this->logger->$level($message, $context); + } + } + + /** + * Deserialize JSON response. + * + * @param string $json Serialized JSON response + * + * @return array + * + * @throws BackendException Deserialization error + */ + protected function deserialize ($json) + { + $response = json_decode($json, true); + $error = json_last_error(); + if ($error != \JSON_ERROR_NONE) { + throw new RuntimeException( + sprintf('JSON decoding error: %s', $error) + ); + } + return $response; + } + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php new file mode 100644 index 00000000000..61178acadf1 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php @@ -0,0 +1,459 @@ +<?php + +/** + * SOLR connector. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> + * @author David Maus <maus@hab.de> + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr; + +use VuFindSearch\Query\AbstractQuery; +use VuFindSearch\Query\QueryGroup; +use VuFindSearch\Query\Query; + +use VuFindSearch\ParamBag; +use VuFindSearch\Query\Params; + +use VuFindSearch\Backend\Exception\RemoteErrorException; +use VuFindSearch\Backend\Exception\RequestErrorException; + +use Zend\Http\Request; +use Zend\Http\Client as HttpClient; + +use Zend\Log\LoggerInterface; + +/** + * SOLR connector. + * + * @category VuFind2 + * @package Search + * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> + * @author David Maus <maus@hab.de> + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class Connector +{ + /** + * Maximum length of a GET url. + * + * Switches to POST if the SOLR target URL exeeds this length. + * + * @see self::sendRequest() + * + * @var integer + */ + const MAX_GET_URL_LENGTH = 2048; + + /** + * Logger instance. + * + * @var LoggerInterface + */ + protected $logger; + + /** + * URL of SOLR core. + * + * @var string + */ + protected $url; + + /** + * HTTP read timeout. + * + * @var float + */ + protected $timeout = 30; + + /** + * Proxy service + * + * @var mixed + */ + protected $proxy; + + /** + * Query invariants. + * + * @var ParamBag + */ + protected $invariants; + + /** + * Last request. + * + * @see self::resubmit() + * + * @var array + */ + protected $lastRequest; + + /** + * Class of HTTP client adapter to use. + * + * @see self::sendRequest() + * + * @var string + */ + protected $httpAdapterClass = 'Zend\Http\Client\Adapter\Socket'; + + /** + * Constructor + * + * @param string $url SOLR base URL + * + * @return void + */ + public function __construct($url) + { + $this->url = $url; + } + + /// Public API + + /** + * Return document specified by id. + * + * @param string $id The document to retrieve from Solr + * + * @return string + */ + public function retrieve ($id) + { + $params = new ParamBag(); + $params->set('q', sprintf('id:"%s"', addcslashes($id, '"'))); + $result = $this->select($params); + return $result; + } + + /** + * Return records similar to a given record specified by id. + * + * Uses MoreLikeThis Request Handler + * + * @param string $id Id of given record + * + * @return string + */ + public function similar ($id) + { + $params = new ParamBag(); + $params->set('q', sprintf('id:"%s"', addcslashes($id, '"'))); + $params->set('qt', 'morelikethis'); + return $this->select($params); + } + + /** + * Execute a search. + * + * @param AbstractQuery $query Search query + * @param Params $params Search parameters + * @param QueryBuilder $queryBuilder Query builder + * + * @return string + */ + public function search (AbstractQuery $query, Params $params, QueryBuilder $queryBuilder) + { + $queryParams = new ParamBag(); + $queryParams->set('start', $params->getOffset()); + $queryParams->set('rows', $params->getLimit()); + $queryParams->set('fl', '*,score'); + $queryParams->set('sort', $params->getSort()); + + $queryParams->mergeWith($queryBuilder->build($query, $params)); + + return $this->select($queryParams); + } + + /** + * Return the current query invariants. + * + * @return ParamBag + */ + public function getQueryInvariants () + { + if (!$this->invariants) { + $this->invariants = new ParamBag(array('wt' => 'json', 'json.nl' => 'arrarr')); + } + return $this->invariants; + } + + /** + * Set the query invariants. + * + * @param array $invariants Query invariants + * + * @return void + */ + public function setQueryInvariants (array $invariants) + { + $this->invariants = new ParamBag($invariants); + } + + /** + * Add a query invariant. + * + * @param string $parameter Query parameter + * @param string $value Query parameter value + * + * @return void + */ + public function addQueryInvariant ($parameter, $value) + { + $this->getQueryInvariants()->add($parameter, $value); + } + + /** + * Set logger instance. + * + * @param LoggerInterface $logger Logger + * + * @return void + */ + public function setLogger (LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Return parameters of the last request. + * + * @return ParamBag + */ + public function getLastRequestParameters () + { + return $this->lastRequest['parameters']; + } + + /** + * Set the HTTP proxy service. + * + * @param mixed $proxy Proxy service + * + * @return void + * + * @todo Typehint on ProxyInterface + */ + public function setProxy ($proxy) + { + $this->proxy = $proxy; + } + + /** + * Set the HTTP connect timeout. + * + * @param float $timeout Timeout in seconds + * + * @return void + */ + public function setTimeout ($timeout) + { + $this->timeout = $timeout; + } + + /** + * Set adapter class of HTTP client. + * + * Keep in mind that a proxy service might replace the client adapter by a + * Proxy adapter if necessary. + * + * @param string $adapterClass Name of adapter class + * + * @return void + */ + public function setHttpAdapterClass ($adapterClass = 'Zend\Http\Client\Adapter\Socket') + { + $this->httpAdapterClass = $adapterClass; + } + + /// Internal API + + /** + * Send request to `select' query handler and return response. + * + * @param ParamBag $params Request parameters + * + * @return array + */ + protected function select (ParamBag $params) + { + $this->prepare($params); + $result = $this->sendRequest('select', $params); + return $result; + } + + /** + * Obtain information from an alphabetic browse index. + * + * @param string $source Name of index to search + * @param string $from Starting point for browse results + * @param int $page Result page to return (starts at 0) + * @param int $page_size Number of results to return on each page + * + * @return string + * + * @todo Deserialize and process the response in backend + */ + public function alphabeticBrowse ($source, $from, $page, $page_size = 20) + { + $params = new ParamBag(); + $params->set('from', $from); + $params->set('offset', $page * $page_size); + $params->set('rows', $page_size); + $params->set('source', $source); + + $url = $this->url . '/browse'; + + $this->prepare($params); + + $result = $this->sendRequest('browse', $params); + return $result; + } + + /** + * Extract terms from the Solr index. + * + * @param string $field Field to extract terms from + * @param string $start Starting term to extract (blank for beginning of list) + * @param int $limit Maximum number of terms to return (-1 for no limit) + * + * @return string + * + * @todo Deserialize and process the response in backend + */ + public function getTerms ($field, $start, $limit) + { + $params = new ParamBag(); + $params->set('terms', 'true'); + $params->set('terms.lower.incl', 'false'); + $params->set('terms.fl', $field); + $params->set('terms.lower', $start); + $params->set('terms.limit', $limit); + $params->set('terms.sort', 'index'); + + $this->prepare($params); + + $result = $this->sendRequest('term', $params); + return $result; + } + + /// Internal API + + /** + * Prepare final request parameters. + * + * This function is called right before the request is send. Adds the + * invariants of our SOLR queries. + * + * @param ParamBag $params Parameters + * + * @return void + */ + protected function prepare (ParamBag $params) + { + $params->mergeWith($this->getQueryInvariants()); + return; + } + + /** + * Repeat the last request with potentially modified parameters. + * + * @see self::getLastRequestParameters() + * + * @return void + */ + public function resubmit () + { + $last = $this->lastRequest; + return $this->sendRequest($last['handler'], $last['parameters'], $last['method']); + } + + /** + * Send request to SOLR and return the response. + * + * @param string $handler SOLR request handler to use + * @param ParamBag $params Request parameters + * @param string $method Request method + * + * @return Zend\Http\Response + * + * @throws RemoteErrorException SOLR signaled a server error (HTTP 5xx) + * @throws RequestErrorException SOLR signaled a client error (HTTP 4xx) + */ + protected function sendRequest ($handler, ParamBag $params, $method = Request::METHOD_GET) + { + $client = new HttpClient(); + $client->setAdapter($this->httpAdapterClass); + $client->setMethod($method); + $client->setOptions(array('timeout' => $this->timeout)); + + $url = $this->url . '/' . $handler; + + $paramString = implode('&', $params->request()); + if (strlen($paramString) > self::MAX_GET_URL_LENGTH) { + $method = Request::METHOD_POST; + } + + if ($method === Request::METHOD_POST) { + $client->setUri($url); + $client->setRawBody($paramString); + $client->setHeaders(array('Content-Type' => Request::ENC_URLENCODED, 'Content-Length' => strlen($paramString))); + } else { + $url = $url . '?' . $paramString; + $client->setUri($url); + } + + if ($this->proxy) { + $this->proxy->proxify($client); + } + + if ($this->logger) { + $this->logger->debug(sprintf('=> %s %s', $client->getMethod(), $client->getUri()), array('params' => $params->request())); + } + + $this->lastRequest = array('parameters' => $params, 'handler' => $handler, 'method' => $method); + + $response = $client->send(); + + if ($this->logger) { + $this->logger->debug(sprintf('<= %s %s', $response->getStatusCode(), $response->getReasonPhrase())); + } + + if (!$response->isSuccess()) { + $status = $response->getStatusCode(); + $phrase = $response->getReasonPhrase(); + if ($status >= 500) { + throw new RemoteErrorException($phrase, $status); + } else { + throw new RequestErrorException($phrase, $status); + } + } + return $response->getBody(); + } +} diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php new file mode 100644 index 00000000000..20fbdfa5a0a --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php @@ -0,0 +1,653 @@ +<?php + +/** + * SOLR QueryBuilder. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> + * @author David Maus <maus@hab.de> + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr; + +use VuFindSearch\Query\AbstractQuery; +use VuFindSearch\Query\QueryGroup; +use VuFindSearch\Query\Query; + +use VuFindSearch\ParamBag; +use VuFindSearch\Query\Params; + +/** + * SOLR QueryBuilder. + * + * @category VuFind2 + * @package Search + * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> + * @author David Maus <maus@hab.de> + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class QueryBuilder +{ + + /** + * Regular expression matching a SOLR range. + * + * @var string + */ + const SOLR_RANGE_RE = '/(\[.+\s+TO\s+.+\])|(\{.+\s+TO\s+.+\})/'; + + /** + * Lookahead that detects whether or not we are inside quotes. + * + * @var string + */ + protected static $insideQuotes = '(?=(?:[^\"]*+\"[^\"]*+\")*+[^\"]*+$)'; + + /** + * Search specs. + * + * @var array + */ + protected $specs; + + /** + * Force ranges to uppercase? + * + * @var boolean + */ + public $caseSensitiveRanges = true; + + /** + * Force boolean operators to uppercase? + * + * @var boolean + */ + public $caseSensitiveBooleans = true; + + /** + * Constructor. + * + * @param array $specs Search handler specifications + * + * @return void + */ + public function __construct (array $specs = array()) + { + $this->setSpecs($specs); + } + + /// Public API + + /** + * Return SOLR search parameters based on a user query and params. + * + * @param AbstractQuery $query User query + * @param Params $params User query params + * + * @return ParamBag + * + * @todo Review usage of filters + * @todo Review usage of facets + * @todo Implement hightlighting + */ + public function build (AbstractQuery $query, Params $params) + { + + if ($query instanceOf QueryGroup) { + $query = $this->reduceQueryGroup($query); + } else { + $query->setString($this->normalizeSearchString($query->getString())); + } + + $searchParams = new ParamBag(); + + // Filters + foreach ($params->getFilters() as $field => $values) { + foreach ($values as $value) { + // Allow trailing wildcards and ranges + if (substr($value, -1) == '*' || preg_match(self::SOLR_RANGE_RE, $value)) { + $searchParams->add('fq', "{$field}:{$value}"); + } else { + $searchParams->add('fq', sprintf('%s:"%s"', $field, addcslashes($value, '"'))); + } + } + } + + // Facet settings + $facets = $params->getFacets(); + if (!empty($facets)) { + $searchParams->set('facet', 'true'); + foreach ($facets as $name => $value) { + $solrName = "facet.{$name}"; + $searchParams->set($solrName, $value); + } + } + + $searchParams->mergeWith( + $this->createSearchParams($query) + ); + return $searchParams; + } + + /** + * Return true if the search string contains advanced Lucene syntax. + * + * @param string $searchString Search string + * + * @return boolean + * + * @todo Maybe factor out to dedicated UserQueryAnalyzer + */ + public function containsAdvancedLuceneSyntax ($searchString) + { + // Check for various conditions that flag an advanced Lucene query: + if ($searchString == '*:*') { + return true; + } + + // The following conditions do not apply to text inside quoted strings, + // so let's just strip all quoted strings out of the query to simplify + // detection. We'll replace quoted phrases with a dummy keyword so quote + // removal doesn't interfere with the field specifier check below. + $searchString = preg_replace('/"[^"]*"/', 'quoted', $searchString); + + // Check for field specifiers: + if (preg_match("/[^\s]\:[^\s]/", $searchString)) { + return true; + } + + // Check for parentheses and range operators: + if (strstr($searchString, '(') && strstr($searchString, ')')) { + return true; + } + $rangeReg = self::SOLR_RANGE_RE; + if (!$this->caseSensitiveRanges) { + $rangeReg .= "i"; + } + if (preg_match($rangeReg, $searchString)) { + return true; + } + + // Build a regular expression to detect booleans -- AND/OR/NOT surrounded + // by whitespace, or NOT leading the query and followed by whitespace. + $boolReg = '/((\s+(AND|OR|NOT)\s+)|^NOT\s+)/'; + if (!$this->caseSensitiveBooleans) { + $boolReg .= "i"; + } + if (preg_match($boolReg, $searchString)) { + return true; + } + + // Check for wildcards and fuzzy matches: + if (strstr($searchString, '*') || strstr($searchString, '?') || strstr($searchString, '~')) { + return true; + } + + // Check for boosts: + if (preg_match('/[\^][0-9]+/', $searchString)) { + return true; + } + + return false; + } + + /** + * Set query builder search specs. + * + * @param array $specs Search specs + * + * @return void + */ + public function setSpecs (array $specs) + { + foreach ($specs as $handler => $spec) { + $this->specs[strtolower($handler)] = new SearchHandler($spec); + } + } + + /** + * Return search specs. + * + * @return array + */ + public function getSpecs () + { + $specs = array(); + foreach ($specs as $handler => $spec) { + $specs[$handler] = $spec->toArray(); + } + return $specs; + } + + /// Internal API + + /** + * Return ParamBag with search query related parameters. + * + * @param Query $query User query + * + * @return ParamBag + * + * @todo Review highlighting + */ + protected function createSearchParams (Query $query) + { + $string = $query->getString() ?: '*:*'; + $handler = $this->getSearchHandler($query->getHandler()); + + $params = new ParamBag(); + + if ($this->containsAdvancedLuceneSyntax($string)) { + + if ($handler) { + // No need to check if hl is enabled or not; hl.q w/o hl=true has no effect + // -- dmaus, 20121107 + $params->set('hl.q', $this->createAdvancedInnerSearchString($string, $handler)); + $string = $this->createAdvancedInnerSearchString($string, $handler); + if ($handler->hasDismax()) { + $string = $handler->createBoostQueryString($string); + } + } + } else { + if ($handler && $handler->hasDismax()) { + $params->set('qf', implode(' ', $handler->getDismaxFields())); + $params->set('qt', 'dismax'); + foreach ($handler->getDismaxParams() as $param) { + foreach ($param as $pair) { + $params->add(reset($pair), next($pair)); + } + } + if ($handler->hasFilterQuery()) { + $params->add('fq', $handler->getFilterQuery()); + } + } else { + if ($handler) { + $string = $handler->createSimpleQueryString($string); + } + } + } + $params->set('q', $string); + return $params; + } + + /** + * Return named search handler. + * + * @param string $handler Search handler name + * + * @return SearchHandler|null + */ + protected function getSearchHandler ($handler) + { + if ($handler && isset($this->specs[$handler])) { + return $this->specs[$handler]; + } else { + return null; + } + } + + /** + * Reduce query group a single query. + * + * @param QueryGroup $group Query group to reduce + * + * @return Query + */ + protected function reduceQueryGroup (QueryGroup $group) + { + $searchString = $this->reduceQueryGroupComponents($group); + $searchHandler = $group->getReducedHandler(); + return new Query($searchString, $searchHandler); + } + + /** + * Reduce components of query group to a search string of a simple query. + * + * This function implements the recursive reduction of a query group. + * + * @param AbstractQuery $component Component + * + * @return string + * + * @see self::reduceQueryGroup() + * + */ + protected function reduceQueryGroupComponents (AbstractQuery $component) + { + if ($component instanceOf QueryGroup) { + $reduced = array_map(array($this, 'reduceQueryGroupComponents'), $component->getQueries()); + $searchString = $component->isNegated() ? 'NOT ' : ''; + $searchString .= sprintf('(%s)', implode(" {$component->getOperator()} ", $reduced)); + } else { + $searchString = $this->normalizeSearchString($component->getString()); + $searchHandler = $this->getSearchHandler($component->getHandler()); + if ($searchHandler) { + $searchString = $this->createSearchString($searchString, $searchHandler); + } + } + return $searchString; + } + + + /** + * Return search string based on input and handler. + * + * @param string $string Input search string + * @param SearchHandler $handler Search handler + * + * @return string + * + */ + protected function createSearchString ($string, SearchHandler $handler = null) + { + $advanced = $this->containsAdvancedLuceneSyntax($string); + + if ($advanced && $handler) { + return $handler->createAdvancedQueryString($string); + } else if ($handler) { + return $handler->createSimpleQueryString($string); + } else { + return $string; + } + } + + /** + * Return advanced inner search string based on input and handler. + * + * @param string $string Input search string + * @param SearchHandler $handler Search handler + * + * @return string + * + */ + protected function createAdvancedInnerSearchString ($string, SearchHandler $handler) + { + // Special case -- if the user wants all records but the current handler + // has a filter query, apply the filter query: + if (trim($string) === '*:*' && $handler && $handler->hasFilterQuery()) { + return $handler->getFilterQuery(); + } + + // Strip out any colons that are NOT part of a field specification: + $string = preg_replace('/(\:\s+|\s+:)/', ' ', $string); + + // If the query already includes field specifications, we can't easily + // apply it to other fields through our defined handlers, so we'll leave + // it as-is: + if (strstr($string, ':')) { + return $string; + } + + // Convert empty queries to return all values in a field: + if (empty($string)) { + $string = '[* TO *]'; + } + + // If the query ends in a question mark, the user may not really intend to + // use the question mark as a wildcard -- let's account for that possibility + if (substr($string, -1) == '?') { + $string = "({$string}) OR (" . substr($string, 0, strlen($string) - 1) . ")"; + } + + return $handler ? $handler->createAdvancedQueryString($string, false) : $string; + } + + /** + * Return normalized input string. + * + * @param string $searchString Input search string + * + * @return string + */ + protected function normalizeSearchString ($searchString) + { + $searchString = $this->prepareForLuceneSyntax($searchString); + + // Force boolean operators to uppercase if we are in a + // case-insensitive mode: + if (!$this->caseSensitiveBooleans) { + $searchString = $this->capitalizeBooleans($searchString); + } + // Adjust range operators if we are in a case-insensitive mode: + if (!$this->caseSensitiveRanges) { + $searchString = $this->capitalizeRanges($searchString); + } + return $searchString; + } + + /** + * Prepare input to be used in a SOLR query. + * + * Handles certain cases where the input might conflict with Lucene + * syntax rules. + * + * @param string $input Input string + * + * @return string + * + * @todo Check if it is safe to assume $input to be an UTF-8 encoded string. + */ + protected function prepareForLuceneSyntax ($input) + { + // Normalize fancy quotes: + $quotes = array( + "\xC2\xAB" => '"', // « (U+00AB) in UTF-8 + "\xC2\xBB" => '"', // » (U+00BB) in UTF-8 + "\xE2\x80\x98" => "'", // ‘ (U+2018) in UTF-8 + "\xE2\x80\x99" => "'", // ’ (U+2019) in UTF-8 + "\xE2\x80\x9A" => "'", // ‚ (U+201A) in UTF-8 + "\xE2\x80\x9B" => "'", // ? (U+201B) in UTF-8 + "\xE2\x80\x9C" => '"', // “ (U+201C) in UTF-8 + "\xE2\x80\x9D" => '"', // †(U+201D) in UTF-8 + "\xE2\x80\x9E" => '"', // „ (U+201E) in UTF-8 + "\xE2\x80\x9F" => '"', // ? (U+201F) in UTF-8 + "\xE2\x80\xB9" => "'", // ‹ (U+2039) in UTF-8 + "\xE2\x80\xBA" => "'", // › (U+203A) in UTF-8 + ); + $input = strtr($input, $quotes); + + // If the user has entered a lone BOOLEAN operator, convert it to lowercase + // so it is treated as a word (otherwise it will trigger a fatal error): + switch(trim($input)) { + case 'OR': + return 'or'; + case 'AND': + return 'and'; + case 'NOT': + return 'not'; + } + + // If the string consists only of control characters and/or BOOLEANs with no + // other input, wipe it out entirely to prevent weird errors: + $operators = array('AND', 'OR', 'NOT', '+', '-', '"', '&', '|'); + if (trim(str_replace($operators, '', $input)) == '') { + return ''; + } + + // Translate "all records" search into a blank string + if (trim($input) == '*:*') { + return ''; + } + + // Ensure wildcards are not at beginning of input + if ((substr($input, 0, 1) == '*') || (substr($input, 0, 1) == '?')) { + $input = substr($input, 1); + } + + // Ensure all parens match + // Better: Remove all parens if they are not balanced + // -- dmaus, 2012-11-11 + $start = preg_match_all('/\(/', $input, $tmp); + $end = preg_match_all('/\)/', $input, $tmp); + if ($start != $end) { + $input = str_replace(array('(', ')'), '', $input); + } + + // Ensure ^ is used properly + // Better: Remove all ^ if not followed by digits + // -- dmaus, 2012-11-11 + $cnt = preg_match_all('/\^/', $input, $tmp); + $matches = preg_match_all('/.+\^[0-9]/', $input, $tmp); + if (($cnt) && ($cnt !== $matches)) { + $input = str_replace('^', '', $input); + } + + // Remove unwanted brackets/braces that are not part of range queries. + // This is a bit of a shell game -- first we replace valid brackets and + // braces with tokens that cannot possibly already be in the query (due + // to ^ normalization in the step above). Next, we remove all remaining + // invalid brackets/braces, and transform our tokens back into valid ones. + // Obviously, the order of the patterns/merges array is critically + // important to get this right!! + $patterns = array( + // STEP 1 -- escape valid brackets/braces + '/\[([^\[\]\s]+\s+TO\s+[^\[\]\s]+)\]/' . + ($this->caseSensitiveRanges ? '' : 'i'), + '/\{([^\{\}\s]+\s+TO\s+[^\{\}\s]+)\}/' . + ($this->caseSensitiveRanges ? '' : 'i'), + // STEP 2 -- destroy remaining brackets/braces + '/[\[\]\{\}]/', + // STEP 3 -- unescape valid brackets/braces + '/\^\^lbrack\^\^/', '/\^\^rbrack\^\^/', + '/\^\^lbrace\^\^/', '/\^\^rbrace\^\^/'); + $matches = array( + // STEP 1 -- escape valid brackets/braces + '^^lbrack^^$1^^rbrack^^', '^^lbrace^^$1^^rbrace^^', + // STEP 2 -- destroy remaining brackets/braces + '', + // STEP 3 -- unescape valid brackets/braces + '[', ']', '{', '}'); + $input = preg_replace($patterns, $matches, $input); + return $input; + } + + /** + * Capitalize boolean operators. + * + * @param string $string Search string + * + * @return string + */ + public function capitalizeBooleans ($string) + { + // Load the "inside quotes" lookahead so we can use it to prevent + // switching case of Boolean reserved words inside quotes, since + // that can cause problems in case-sensitive fields when the reserved + // words are actually used as search terms. + $lookahead = self::$insideQuotes; + $regs = array("/\s+AND\s+{$lookahead}/i", "/\s+OR\s+{$lookahead}/i", + "/(\s+NOT\s+|^NOT\s+){$lookahead}/i", "/\(NOT\s+{$lookahead}/i"); + $replace = array(' AND ', ' OR ', ' NOT ', '(NOT '); + return trim(preg_replace($regs, $replace, $string)); + } + + /** + * Capitalize range operator. + * + * @param string $string Search string + * + * @return string + */ + public function capitalizeRanges ($string) + { + // Load the "inside quotes" lookahead so we can use it to prevent + // switching case of ranges inside quotes, since that can cause + // problems in case-sensitive fields when the reserved words are + // actually used as search terms. + $lookahead = self::$insideQuotes; + $regs = array("/(\[)([^\]]+)\s+TO\s+([^\]]+)(\]){$lookahead}/i", + "/(\{)([^}]+)\s+TO\s+([^}]+)(\}){$lookahead}/i"); + $callback = array($this, 'capitalizeRangesCallback'); + return trim(preg_replace_callback($regs, $callback, $string)); + } + + /** + * Callback helper function. + * + * @param array $match Matches as of preg_replace_callback() + * + * @return string + * + * @see self::capitalizeRanges + * + * @todo Check possible problem with umlauts/non-ASCII word characters + */ + protected function capitalizeRangesCallback ($match) + { + // Extract the relevant parts of the expression: + $open = $match[1]; // opening symbol + $close = $match[4]; // closing symbol + $start = $match[2]; // start of range + $end = $match[3]; // end of range + + // Is this a case-sensitive range? + if (strtoupper($start) != strtolower($start) + || strtoupper($end) != strtolower($end) + ) { + // Build a lowercase version of the range: + $lower = $open . trim(strtolower($start)) . ' TO ' . + trim(strtolower($end)) . $close; + // Build a uppercase version of the range: + $upper = $open . trim(strtoupper($start)) . ' TO ' . + trim(strtoupper($end)) . $close; + + // Special case: don't create illegal timestamps! + $timestamp = '/[0-9]{4}-[0-9]{2}-[0-9]{2}t[0-9]{2}:[0-9]{2}:[0-9]{2}z/i'; + if (preg_match($timestamp, $start) || preg_match($timestamp, $end)) { + return $upper; + } + + // Accept results matching either range: + return '(' . $lower . ' OR ' . $upper . ')'; + } else { + // Simpler case -- case insensitive (probably numeric) range: + return $open . trim($start) . ' TO ' . trim($end) . $close; + } + } + + /** + * Return from and to values of SOLR range. + * + * Returns false if the search string does not contain a range. Otherwise + * it returns an associative array with the keys `from', and `to' + * referring to the lower and upper bound of the range. + * + * @param string $string Search string + * + * @return array|false + * + * @todo This function seems to be unused + */ + public function parseRange ($string) + { + $regEx = '/\[([^\]]+)\s+TO\s+([^\]]+)\]/'; + if (!preg_match($regEx, $string, $matches)) { + return false; + } + return array('from' => trim($matches[1]), 'to' => trim($matches[2])); + } + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php new file mode 100644 index 00000000000..7ccd3a89159 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php @@ -0,0 +1,108 @@ +<?php + +/** + * Simple, schema-less SOLR record. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr\Response\Json; + +use VuFindSearch\Response\RecordInterface; + +/** + * Simple, schema-less SOLR record. + * + * This record primarily servers as an example or blueprint for a schema-less + * record. All SOLR fields are exposed via object properties. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class Record implements RecordInterface +{ + + /** + * SOLR fields. + * + * @var array + */ + protected $fields; + + /** + * Source identifier. + * + * @var string + */ + protected $source; + + /** + * Constructor. + * + * @param array $fields SOLR document fields + * + * @return void + */ + public function __construct (array $fields) + { + $this->fields = $fields; + } + + /** + * Set the source backend identifier. + * + * @param string $identifier Backend identifier + * + * @return void + */ + public function setSourceIdentifier ($identifier) + { + $this->source = $identifier; + } + + /** + * Return the source backend identifier. + * + * @return string + */ + public function getSourceIdentifier () + { + return $this->source; + } + + /** + * __get() + * + * @param string $name Field name + * + * @return mixed + */ + public function __get ($name) + { + return isset($this->fields[$name]) ? $this->fields[$name] : null; + } +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php new file mode 100644 index 00000000000..ff59ad266ee --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php @@ -0,0 +1,243 @@ +<?php + +/** + * Simple JSON-based record collection. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr\Response\Json; + +use VuFindSearch\Response\RecordCollectionInterface; +use VuFindSearch\Response\RecordInterface; + +use VuFindSearch\Exception\RuntimeException; + +/** + * Simple JSON-based record collection. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class RecordCollection implements RecordCollectionInterface +{ + + /** + * Deserialized SOLR response. + * + * @var array + */ + protected $response; + + /** + * Response records. + * + * @var array + */ + protected $records; + + /** + * Constructor. + * + * @param array $response Deserialized SOLR response + * + * @return void + */ + public function __construct (array $response) + { + $this->response = $response; + $this->offset = $response['response']['start']; + $this->records = array(); + $this->rewind(); + } + + /** + * Return total number of records found. + * + * @return int + */ + public function getTotal () + { + return $this->response['response']['numFound']; + } + + /** + * Return query time in milli-seconds. + * + * @return float + */ + public function getQueryTime () + { + return $this->response['responseHeader']['QTime']; + } + + /** + * Return available facets. + * + * Returns associative array of facets indexed by facet field. The value + * is a numeric array with available facets for this field. Each facet is + * a numeric array with the facet value followed by the facet count. + * + * @return array + */ + public function getFacets () + { + return $this->response['response']['facet_fields']; + } + + /** + * Return records. + * + * @return array + */ + public function getRecords () + { + return $this->records; + } + + /** + * Return offset in the total search result set. + * + * @return int + */ + public function getOffset () + { + return $this->response['response']['start']; + } + + /** + * Return first record in response. + * + * @return RecordInterface|null + */ + public function first () + { + return isset($this->records[$this->offset]) ? $this->records[$this->offset] : null; + } + + /** + * Set the source backend identifier. + * + * @param string $identifier Backend identifier + * + * @return void + */ + public function setSourceIdentifier ($identifier) + { + $this->source = $identifier; + } + + /** + * Return the source backend identifier. + * + * @return string + */ + public function getSourceIdentifier () + { + return $this->source; + } + + /** + * Add a record to the collection. + * + * @param RecordInterface $record Record to add + * + * @return void + */ + public function add (RecordInterface $record) + { + if (!in_array($record, $this->records, true)) { + $this->records[$this->pointer] = $record; + $this->next(); + } + } + + /// Iterator interface + + /** + * Return true if current collection index is valid. + * + * @return boolean + */ + public function valid () + { + return isset($this->records[$this->pointer]); + } + + /** + * Return record at current collection index. + * + * @return RecordInterface + */ + public function current () + { + return $this->records[$this->pointer]; + } + + /** + * Rewind collection index. + * + * @return void + */ + public function rewind () + { + $this->pointer = $this->offset; + } + + /** + * Move to next collection index. + * + * @return void + */ + public function next () + { + $this->pointer++; + } + + /** + * Return current collection index. + * + * @return integer + */ + public function key () + { + return $this->pointer; + } + + /// Countable interface + + /** + * Return number of records in collection. + * + * @return integer + */ + public function count () + { + return count($this->records); + } + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php new file mode 100644 index 00000000000..118dd71d42d --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php @@ -0,0 +1,93 @@ +<?php + +/** + * Simple JSON-based factory for record collection. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr\Response\Json; + +use VuFindSearch\Response\RecordCollectionFactoryInterface; +use VuFindSearch\Exception\InvalidArgumentException; + +/** + * Simple JSON-based factory for record collection. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class RecordCollectionFactory implements RecordCollectionFactoryInterface +{ + /** + * Class of collection records. + * + * @var string + */ + protected $recordClass; + + /** + * Class of collection. + * + * @var string + */ + protected $collectionClass; + + /** + * Constructor. + * + * @param string $recordClass Class of collection records + * @param string $collectionClass Class of collection + * + * @return void + */ + public function __construct ($recordClass = 'VuFindSearch\Backend\Solr\Response\Json\Record', $collectionClass = 'VuFindSearch\Backend\Solr\Response\Json\RecordCollection') + { + $this->recordClass = $recordClass; + $this->collectionClass = $collectionClass; + } + + /** + * Return record collection. + * + * @param array $response Deserialized JSON response + * + * @return RecordCollection + */ + public function factory ($response) + { + if (!is_array($response)) { + throw new InvalidArgumentException(sprintf('Unexpected type of value: Expected array, got %s', gettype($response))); + } + $collection = new $this->collectionClass($response); + foreach ($response['response']['docs'] as $doc) { + $collection->add(new $this->recordClass($doc)); + } + return $collection; + } + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php new file mode 100644 index 00000000000..500ca525b93 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php @@ -0,0 +1,448 @@ +<?php + +/** + * VuFind SearchHandler. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> + * @author David Maus <maus@hab.de> + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Backend\Solr; + +/** + * VuFind SearchHandler. + * + * The SearchHandler implements the rule-based translation of a user search + * query to a SOLR query string. + * + * @category VuFind2 + * @package Search + * @author Andrew S. Nagy <vufind-tech@lists.sourceforge.net> + * @author David Maus <maus@hab.de> + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class SearchHandler +{ + + /** + * Known configuration keys. + * + * @var array + */ + protected static $configKeys = array( + 'CustomMunge', 'DismaxFields', 'QueryFields', 'DismaxParams', 'FilterQuery' + ); + + /** + * Known boolean operators. + * + * @var array + */ + protected static $booleanOperators = array('AND', 'OR', 'NOT'); + + /** + * Search handler specification. + * + * @var array + */ + protected $specs; + + /** + * Constructor. + * + * @param array $spec Search handler specification + * + * @return void + */ + public function __construct (array $spec) + { + foreach (self::$configKeys as $key) { + $this->specs[$key] = isset($spec[$key]) ? $spec[$key] : array(); + } + } + + /// Public API + + /** + * Return an advanced query string. + * + * An advanced query string is a query string based on a search string w/ + * lucene syntax features. + * + * @param string $search Search string + * + * @return string + * + * @see \VuFind\Service\Solr\QueryBuilder::containsAdvancedLuceneSyntax() + */ + public function createAdvancedQueryString ($search) + { + return $this->createQueryString($search, true); + } + + /** + * Return a simple query string. + * + * @param string $search Search string + * + * @return string + * + * @see \VuFind\Service\Solr\SearchHandler::createAdvancedQueryString() + */ + public function createSimpleQueryString ($search) + { + return $this->createQueryString($search, false); + } + + /** + * Return an advanced query string for specified search string. + * + * @param string $search Search string + * + * @return string + */ + public function createBoostQueryString ($search) + { + $boostQuery = array(); + if ($this->hasDismax()) { + foreach ($this->getDismaxParams() as $param) { + list($name, $value) = $param; + if ($name === 'bq') { + $boostQuery[] = $value; + } else if ($name === 'bf') { + // BF parameter may contain multiple space-separated functions + // with individual boosts. We need to parse this into _val_ + // query components: + foreach (explode(' ', $value) as $boostFunction) { + if ($boostFunction) { + $parts = explode('^', $boostFunction, 2); + $boostQuery[] = sprintf( + '_val_:"%s"%s', + addcslashes($parts[0], '"'), + isset($parts[1]) ? "^{$parts[1]}" : '' + ); + } + } + } + } + } + if ($boostQuery) { + return sprintf( + '(%s) AND (*:* OR %s)', + $search, implode(' OR ', $boostQuery) + ); + } else { + return $search; + } + } + + /** + * Return true if the handler defines Dismax fields. + * + * @return boolean + */ + public function hasDismax () + { + return !empty($this->specs['DismaxFields']); + } + + /** + * Return defined dismax fields. + * + * @return array + */ + public function getDismaxFields () + { + return $this->specs['DismaxFields']; + } + + /** + * Return defined dismax parameters. + * + * @return array + */ + public function getDismaxParams () + { + return $this->specs['DismaxParams']; + } + + /** + * Return the filter query. + * + * @return string + * + */ + public function getFilterQuery () + { + return empty($this->specs['FilterQuery']) ? null : $this->specs['FilterQuery']; + } + + /** + * Return true if handler defines a filter query. + * + * @return boolean + */ + public function hasFilterQuery () + { + return (bool)$this->specs['FilterQuery']; + } + + /** + * Serialize handler specs as array. + * + * @return array + */ + public function toArray () + { + return $this->specs; + } + + /// Internal API + + /** + * Return a Dismax subquery for specified search string. + * + * @param string $search Search string + * + * @return string + */ + protected function dismaxSubquery ($search) + { + $dismaxParams = array(); + foreach ($this->specs['DismaxParams'] as $param) { + $dismaxParams[] = sprintf( + "%s='%s'", $param[0], addcslashes($param[1], "'") + ); + } + $dismaxQuery = sprintf( + '{!dismax qf="%s" %s}%s', + implode(' ', $this->specs['DismaxFields']), + implode(' ', $dismaxParams), + $search + ); + return sprintf('_query_:"%s"', addslashes($dismaxQuery)); + } + + /** + * Return the munge values for specified search string. + * + * If optional argument $tokenize is true tokenize the search string. + * + * @param string $search Search string + * @param boolean $tokenize Tokenize the search string? + * + * @return string + */ + protected function mungeValues ($search, $tokenize = true) + { + if ($tokenize) { + $tokens = $this->tokenize($search); + $mungeValues = array( + 'onephrase' => sprintf( + '"%s"', str_replace('"', '', implode(' ', $tokens)) + ), + 'and' => implode(' AND ', $tokens), + 'or' => implode(' OR ', $tokens), + ); + } else { + $mungeValues = array( + 'and' => $search, + 'or' => $search, + ); + // If we're skipping tokenization, we just want to pass $lookfor through + // unmodified (it's probably an advanced search that won't benefit from + // tokenization). We'll just set all possible values to the same thing, + // except that we'll try to do the "one phrase" in quotes if possible. + // IMPORTANT: If we detect a boolean NOT, we MUST omit the quotes. + if (strstr($search, '"') || strstr($search, ' NOT ')) { + $mungeValues['onephrase'] = $search; + } else { + $mungeValues['onephrase'] = sprintf('"%s"', $search); + } + } + + foreach ($this->specs['CustomMunge'] as $mungeName => $mungeOps) { + $mungeValues[$mungeName] = $search; + foreach ($mungeOps as $operation) { + switch ($operation[0]) { + case 'append': + $mungeValues[$mungeName] .= $operation[1]; + break; + case 'lowercase': + $mungeValues[$mungeName] = strtolower($mungeValues[$mungeName]); + break; + case 'preg_replace': + $mungeValues[$mungeName] = preg_replace( + $operation[1], $operation[2], $mungeValues[$mungeName] + ); + break; + case 'uppercase': + $mungeValues[$mungeName] = strtoupper($mungeValues[$mungeName]); + break; + default: + throw new \InvalidArgumentException( + sprintf('Unknown munge operation: %s', $operation[0]) + ); + } + } + } + return $mungeValues; + } + + /** + * Return query string for specified search string. + * + * If optional argument $advanced is true the search string contains + * advanced lucene query syntax. + * + * @param string $search Search string + * @param boolean $advanced Is the search an advanced search string? + * + * @return string + * + */ + protected function createQueryString ($search, $advanced = false) + { + + // If this is a basic query and we have Dismax settings, let's build + // a Dismax subquery to avoid some of the ugly side effects of our Lucene + // query generation logic. + if (!$advanced && $this->hasDismax()) { + $query = $this->dismaxSubquery($search); + } else { + $mungeRules = $this->mungeRules(); + // Do not munge w/o rules + if ($mungeRules) { + $mungeValues = $this->mungeValues($search, !$advanced); + $query = $this->munge($mungeRules, $mungeValues); + } else { + $query = $search; + } + } + if ($this->hasFilterQuery()) { + $query = sprintf('(%s) AND (%s)', $query, $this->getFilterQuery()); + } + return "($query)"; + } + + /** + * Return array of munge rules. + * + * @todo Maybe rename? + * + * @return array + */ + protected function mungeRules () + { + return $this->specs['QueryFields']; + } + + /** + * Return modified search strign after applying the transformation rules. + * + * @param array $mungeRules Munge rules + * @param array $mungeValues Munge values + * @param string $joiner Joiner of subqueries + * + * @return string + */ + protected function munge (array $mungeRules, array $mungeValues, $joiner = 'OR') + { + $clauses = array(); + foreach ($mungeRules as $field => $clausearray) { + if (is_numeric($field)) { + // shift off the join string and weight + $sw = array_shift($clausearray); + $internalJoin = ' ' . $sw[0] . ' '; + // Build it up recursively + $sstring = '(' . + $this->munge($clausearray, $mungeValues, $internalJoin) . + ')'; + // ...and add a weight if we have one + $weight = $sw[1]; + if (!is_null($weight) && $weight && $weight > 0) { + $sstring .= '^' . $weight; + } + // push it onto the stack of clauses + $clauses[] = $sstring; + } else { + // Otherwise, we've got a (list of) [munge, weight] pairs to deal + // with + foreach ($clausearray as $spec) { + // build a string like title:("one two") + $sstring = $field . ':(' . $mungeValues[$spec[0]] . ')'; + // Add the weight if we have one. Yes, I know, it's redundant + // code. + $weight = $spec[1]; + if (!is_null($weight) && $weight && $weight > 0) { + $sstring .= '^' . $weight; + } + // ..and push it on the stack of clauses + $clauses[] = $sstring; + } + } + } + + // Join it all together + return implode(' ' . $joiner . ' ', $clauses); + } + + /** + * Tokenize the search string. + * + * @param string $string Search string + * + * @return array + */ + protected function tokenize ($string) + { + // Tokenize on spaces and quotes + $phrases = array(); + preg_match_all('/"[^"]*"[~[0-9]+]*|"[^"]*"|[^ ]+/', $string, $phrases); + $phrases = $phrases[0]; + + $tokens = array(); + $token = array(); + + reset($phrases); + while (current($phrases) !== false) { + $token[] = current($phrases); + $next = next($phrases); + if (in_array($next, self::$booleanOperators)) { + $token[] = $next; + if (next($phrases) === false) { + $tokens[] = implode(' ', $token); + } + } else { + $tokens[] = implode(' ', $token); + $token = array(); + } + } + + return $tokens; + } +} diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php b/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php new file mode 100644 index 00000000000..778eccce2be --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php @@ -0,0 +1,43 @@ +<?php + +/** + * Marker interface. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ + +namespace VuFindSearch\Exception; + +/** + * Marker interface. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ +interface ExceptionInterface +{ +} diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php b/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php new file mode 100644 index 00000000000..ebe0b00ffb0 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php @@ -0,0 +1,42 @@ +<?php + +/** + * Invalid argument exception. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Exception; + +/** + * Invalid argument exception. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php b/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php new file mode 100644 index 00000000000..dfd51ba60d6 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php @@ -0,0 +1,43 @@ +<?php + +/** + * Generic RuntimeException. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ + +namespace VuFindSearch\Exception; + +/** + * Generic RuntimeException. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/ParamBag.php b/module/VuFindSearch/src/VuFindSearch/ParamBag.php new file mode 100644 index 00000000000..56d529b7b81 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/ParamBag.php @@ -0,0 +1,199 @@ +<?php + +/** + * Parameter bag. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch; + +/** + * Lightweight wrapper for request parameters. + * + * This class represents the request parameters. Parameters are stored in an + * associative array with the parameter name as key. Because e.g. SOLR allows + * repeated query parameters the values are always stored in an array. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class ParamBag +{ + + /** + * Parameters + * + * @var array + */ + protected $params = array(); + + /** + * Constructor. + * + * @param array $initial Initial parameters + * + * @return void + */ + public function __construct (array $initial = array()) + { + foreach ($initial as $name => $value) { + $this->add($name, $value); + } + } + + /** + * Return parameter value. + * + * @param string $name Parameter name + * + * @return mixed|null Parameter value or NULL if not set + */ + public function get ($name) + { + return isset($this->params[$name]) ? $this->params[$name] : null; + } + + /** + * Set a parameter. + * + * @param string $name Parameter name + * @param string $value Parameter value + * + * @return void + */ + public function set ($name, $value) + { + if (is_array($value)) { + $this->params[$name] = $value; + } else { + $this->params[$name] = array($value); + } + } + + /** + * Remove a parameter. + * + * @param string $name Parameter name + * + * @return void + */ + public function remove ($name) + { + if (isset($this->params[$name])) { + unset($this->params[$name]); + } + } + + /** + * Add parameter value. + * + * @param string $name Parameter name + * @param mixed $value Parameter value + * + * @return void + */ + public function add ($name, $value) + { + if (!isset($this->params[$name])) { + $this->params[$name] = array(); + } + if (is_array($value)) { + $this->params[$name] = array_merge($this->params[$name], $value); + } else { + $this->params[$name][] = $value; + } + } + + /** + * Return array of request parameters. + * + * @return array + */ + public function params () + { + return $this->params; + } + + /** + * Merge with another parameter bag. + * + * @param ParamBag $bag Parameter bag to merge with + * + * @return void + */ + public function mergeWith (ParamBag $bag) + { + foreach ($bag->params as $key => $value) { + if (!empty($value)) { + $this->add($key, $value); + } + } + } + + /** + * Merge with all supplied parameter bags. + * + * @param array $bags Parameter bags to merge with + * + * @return void + */ + public function mergeWithAll (array $bags) + { + foreach ($bags as $bag) { + $this->mergeWith($bag); + } + } + + /** + * Return array of params ready to be used in a HTTP request. + * + * Returns a numerical array with all request parameters as properly URL + * encoded key-value pairs. + * + * @return array + */ + public function request () + { + $request = array(); + foreach ($this->params as $name => $values) { + if (!empty($values)) { + $request = array_merge( + $request, + array_map( + function ($value) use ($name) { + return sprintf('%s=%s', urlencode($name), urlencode($value)); + }, + $values + ) + ); + } + } + return $request; + } + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php b/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php new file mode 100644 index 00000000000..620a9649c61 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php @@ -0,0 +1,42 @@ +<?php + +/** + * Abstract base class of user query components. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Query; + +/** + * Abstract base class of user query components. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +abstract class AbstractQuery +{} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Query/Params.php b/module/VuFindSearch/src/VuFindSearch/Query/Params.php new file mode 100644 index 00000000000..314cf92b375 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Query/Params.php @@ -0,0 +1,269 @@ +<?php + +/** + * Query parameter class file. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category Search + * @package Query + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/system_classes Wiki + */ + + +namespace VuFindSearch\Query; + +/** + * Query parameter class. + * + * @category Search + * @package Query + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/system_classes Wiki + */ + +class Params +{ + + /** + * Sort order. + * + * @var string + */ + protected $sort = 'relevance'; + + /** + * Maximum number of records per result. + * + * @var int + */ + protected $limit = 10; + + /** + * Offset in entire result set. + * + * @var int + */ + protected $offset = 0; + + /** + * Facet settings. + * + * @var array + */ + protected $facets = array(); + + /** + * Filter settings. + * + * @var array + */ + protected $filters = array(); + + /** + * Spellcheck dictionary. + * + * @var string + */ + protected $spellcheckDictionary; + + /** + * Is spellcheck enabled? + * + * @var boolean + */ + protected $spellcheckEnabled; + + /** + * Return facet settings. + * + * @return array + */ + public function getFacets () + { + return $this->facets; + } + + /** + * Set facet settings. + * + * @param array $facets Facet settings + * + * @return void + */ + public function setFacets (array $facets) + { + $this->facets = $facets; + } + + /** + * Return filter settings. + * + * @return array + */ + public function getFilters () + { + return $this->filters; + } + + /** + * Set filter settings. + * + * @param string $field Filter field + * @param string $value Filter value + * + * @return void + */ + public function setFilter ($field, $value) + { + $this->filters[$field] = $value; + } + + /** + * Return selected shards. + * + * @return array + */ + public function getShards () + { + return array(); + } + + /** + * Return spellcheck dictionary. + * + * @return array + */ + public function getSpellcheckDictionary () + { + return $this->spellcheckDictionary; + } + + /** + * Set spellcheck dictionary. + * + * @param string $dictionary + * @return void + */ + public function setSpellcheckDictionary ($dictionary) + { + $this->spellcheckDictionary = $dictionary; + } + + /** + * Return true if spellcheck suggestions are enabled. + * + * @return boolean + */ + public function isSpellcheckEnabled () + { + return $this->spellcheckEnabled; + } + + /** + * Enable or disable spellcheck. + * + * @param boolean $enable + * @return void + */ + public function enableSpellcheck ($enable) + { + $this->spellcheckEnabled = (boolean)$enable; + } + + /** + * Return highlighting settings. + * + * Dummy implementation + * + * @return array + */ + public function getHighlighting () + { + return array(); + } + + /** + * Return sort order. + * + * @return string + */ + public function getSort () + { + return $this->sort; + } + + /** + * Set sort order. + * + * @param string $order + * + * @return void + */ + public function setSort ($sort) + { + $this->sort = (string)$sort; + } + + /** + * Return limit. + * + * @return int + */ + public function getLimit () + { + return $this->limit; + } + + /** + * Set limit. + * + * @param int $limit + * + * @return void + */ + public function setLimit ($limit) + { + $this->limit = (int)$limit; + } + + /** + * Return offset. + * + * @return int + */ + public function getOffset () + { + return $this->offset; + } + + /** + * Set offset. + * + * @param int $offset + * + * @return void + */ + public function setOffset ($offset) + { + $this->offset = (int)$offset; + } +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Query/Query.php b/module/VuFindSearch/src/VuFindSearch/Query/Query.php new file mode 100644 index 00000000000..41e5bace2da --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Query/Query.php @@ -0,0 +1,120 @@ +<?php + +/** + * A single/simple query. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Query; + +/** + * A single/simple query. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class Query extends AbstractQuery +{ + + /** + * Name of query handler, if any. + * + * @var string + */ + protected $queryHandler; + + /** + * Query string + * + * @var string + */ + protected $queryString; + + /** + * Constructor. + * + * @param string $string Search string + * @param string $handler Name of search handler + * + * @return void + */ + public function __construct ($string = null, $handler = null) + { + $this->queryHandler = $handler ? strtolower($handler) : null; + $this->queryString = $string; + } + + /** + * Return search string. + * + * @return string + */ + public function getString () + { + return $this->queryString; + } + + /** + * Set the search string. + * + * @param string $string New search string + * + * @return void + */ + public function setString ($string) + { + $this->queryString = $string; + } + + /** + * Return name of search handler. + * + * @return string + */ + public function getHandler () + { + return $this->queryHandler; + } +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php b/module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php new file mode 100644 index 00000000000..7a30d86328c --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php @@ -0,0 +1,71 @@ +<?php + +/** + * QueryAdapter class file. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category Search + * @package Query + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/system_classes Wiki + */ + +namespace VuFindSearch\Query; + +/** + * Adapter for legacy query representation. + * + * @category Search + * @package Query + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/system_classes Wiki + */ + +abstract class QueryAdapter +{ + + /** + * Return a Query or QueryGroup based on user search arguments. + * + * @param array $search Search arguments + * + * @return \VuFind\Search\Query|\VuFind\Search\QueryGroup + */ + public static function create (array $search) + { + if (isset($search['lookfor'])) { + $handler = isset($search['index']) ? $search['index'] : $search['field']; + return new Query($search['lookfor'], $handler); + } elseif (isset($search['group'])) { + $operator = $search['group'][0]['bool']; + return new QueryGroup($operator, array_map(array('self', 'create'), $search['group'])); + } else { + // Special case: The outer-most group-of-groups. + if (isset($search[0]['join'])) { + $operator = $search[0]['join']; + return new QueryGroup($operator, array_map(array('self', 'create'), $search)); + } else { + // Simple query + return new Query($search[0]['lookfor'], $search[0]['index']); + } + } + } +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php b/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php new file mode 100644 index 00000000000..7403c0f3e84 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php @@ -0,0 +1,210 @@ +<?php + +/** + * A group of single/simples queries, joined by boolean operator. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch\Query; + +use VuFindSearch\Exception\InvalidArgumentException; + +/** + * A group of single/simples queries, joined by boolean operator. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class QueryGroup extends AbstractQuery +{ + + /** + * Valid boolean operators. + * + * @var array + */ + protected static $operators = array('AND', 'OR', 'NOT'); + + /** + * Name of the handler to be used if the query group is reduced. + * + * @see VuFindSearch\Backend\Solr\QueryBuilder::reduceQueryGroup() + * + * @var string + * + * @todo Check if we actually use/need this feature + */ + protected $reducedHandler; + + /** + * Boolean operator. + * + * @var string + */ + protected $operator; + + /** + * Is the query group negated? + * + * @var boolean + */ + protected $negation; + + /** + * Queries. + * + * @var array + */ + protected $queries; + + /** + * Constructor. + * + * @param string $operator Boolean operator + * @param array $queries Queries + * @param string $reducedHandler Handler to be uses if reduced + * + * @return void + */ + public function __construct ($operator, array $queries = array(), $reducedHandler = null) + { + $this->setOperator($operator); + $this->setQueries($queries); + $this->setReducedHandler($reducedHandler); + } + + /** + * Return name of reduced handler. + * + * @return string|null + */ + public function getReducedHandler () + { + return $this->reducedHandler; + } + + /** + * Set name of reduced handler. + * + * @param string $handler Reduced handler + * + * @return void + */ + public function setReducedHandler ($handler) + { + $this->reducedHandler = $handler; + } + + /** + * Unset reduced handler. + * + * @return void + */ + public function unsetReducedHandler () + { + $this->reducedHandler = null; + } + + /** + * Add a query to the group. + * + * @param \VuFind\Search\AbstractQuery $query Query to add + * + * @return void + */ + public function addQuery (AbstractQuery $query) + { + $this->queries[] = $query; + } + + /** + * Return group queries. + * + * @return array + */ + public function getQueries () + { + return $this->queries; + } + + /** + * Set group queries. + * + * @param array $queries Group queries + * + * @return void + */ + public function setQueries (array $queries) + { + foreach ($queries as $query) { + $this->addQuery($query); + } + } + + /** + * Set boolean operator. + * + * @param string $operator Boolean operator + * + * @return void + * + * @throws \InvalidArgumentException Unknown or invalid boolean operator + */ + public function setOperator ($operator) + { + if (!in_array($operator, self::$operators)) { + throw new InvalidArgumentException("Unknown or invalid boolean operator: {$operator}"); + } + if ($operator == 'NOT') { + $this->operator = 'OR'; + $this->negation = true; + } else { + $this->operator = $operator; + } + } + + /** + * Return boolean operator. + * + * @return string + */ + public function getOperator () + { + return $this->operator; + } + + /** + * Return true if group is an exclusion group. + * + * @return boolean + */ + public function isNegated () + { + return $this->negation; + } +} diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php new file mode 100644 index 00000000000..ac874cee074 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php @@ -0,0 +1,8 @@ +<?php + +namespace VuFindSearch\Response; + +interface RecordCollectionFactoryInterface +{ + public function factory ($response); +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php new file mode 100644 index 00000000000..0ec8069b0fc --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php @@ -0,0 +1,115 @@ +<?php + +/** + * Search backend search response interface file. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category Search + * @package Service + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ + +namespace VuFindSearch\Response; + +/** + * Interface for backend responses to a search() operation. + * + * @category Search + * @package Service + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ +interface RecordCollectionInterface extends \Countable, \Iterator +{ + + /** + * Return total number of records found. + * + * @return int + */ + public function getTotal (); + + /** + * Return query time in milli-seconds. + * + * @return float + */ + public function getQueryTime (); + + /** + * Return available facets. + * + * Returns an associative array with the internal field name as key. The + * value is an associative array of the available facets for the field, + * indexed by facet value. + * + * @return array + */ + public function getFacets (); + + /** + * Return records. + * + * @return array + */ + public function getRecords (); + + /** + * Return offset in the total search result set. + * + * @return int + */ + public function getOffset (); + + /** + * Return first record in collection. + * + * @return RecordInterface|null + */ + public function first (); + + /** + * Set the source backend identifier. + * + * @param string $identifier Backend identifier + * + * @return void + */ + public function setSourceIdentifier ($identifier); + + /** + * Return the source backend identifier. + * + * @return string + */ + public function getSourceIdentifier (); + + /** + * Add a record to the collection. + * + * @param RecordInterface $record + * + * @return void + */ + public function add (RecordInterface $record); + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php new file mode 100644 index 00000000000..305c8a3f9b0 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php @@ -0,0 +1,62 @@ +<?php + +/** + * Record interface file. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category Search + * @package Service + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ + +namespace VuFindSearch\Response; + +/** + * Record interface. + * + * Every record must implement this. + * + * @category Search + * @package Service + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://github.com/dmj/vf2-search-subsystem + */ +interface RecordInterface +{ + + /** + * Set the source backend identifier. + * + * @param string $identifier Backend identifier + * + * @return void + */ + public function setSourceIdentifier ($identifier); + + /** + * Return the source backend identifier. + * + * @return string + */ + public function getSourceIdentifier (); + +} \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Service.php b/module/VuFindSearch/src/VuFindSearch/Service.php new file mode 100644 index 00000000000..533aea4c5ff --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Service.php @@ -0,0 +1,296 @@ +<?php + +/** + * Search service. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindSearch; + +use VuFindSearch\Backend\BackendInterface; + +use Zend\Log\LoggerInterface; +use Zend\EventManager\EventManagerInterface; +use Zend\EventManager\EventManager; + +/** + * Search service. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class Service +{ + + /** + * Event manager. + * + * @var EventManager + */ + protected $events; + + /** + * Logger, if any. + * + * @var LoggerInterface + */ + protected $logger; + + /** + * Cache resolved backends. + * + * @var array + */ + protected $backends; + + /** + * Constructor. + * + * @return void + */ + public function __construct () + { + $this->backends = array(); + } + + /** + * Perform a search and return a wrapped response. + * + * @param string $backend Search backend identifier + * @param AbstractQuery $query Search query + * @param Params $params Search parameters + * + * @return ResponseInterface + * + */ + public function search ($backend, Query\AbstractQuery $query, Query\Params $params) + { + $context = __FUNCTION__; + $args = compact('backend', 'query', 'params', 'context'); + $backend = $this->resolve($backend, $args); + + $this->triggerPre($context, $backend, $args); + $response = $backend->search($query, $params); + $this->triggerPost($context, $response, $args); + return $response; + } + + /** + * Retrieve a single record. + * + * @param string $backend Search backend identifier + * @param string $id Record identifier + * + * @return ResponseInterface + */ + public function retrieve ($backend, $id) + { + $context = __FUNCTION__; + $args = compact('backend', 'id', 'context'); + $backend = $this->resolve($backend, $args); + + $this->triggerPre($context, $backend, $args); + $response = $backend->retrieve($id); + $this->triggerPost($context, $response, $args); + return $response; + } + + /** + * Delete a single record. + * + * @param string $backend Search backend identifier + * @param string $id Record identifier + * + * @return null + */ + public function delete ($backend, $id) + { + $context = __FUNCTION__; + $args = compact('backend', 'id', 'context'); + $backend = $this->resolve($backend, $args); + + $this->triggerPre($context, $backend, $args); + $response = $backend->delete($id); + $this->triggerPost($context, $response, $args); + } + + /** + * Delete all records. + * + * @param string $backend Search backend identifier + * + * @return null + */ + public function deleteAll ($backend) + { + $context = __FUNCTION__; + $args = compact('backend', 'context'); + $backend = $this->resolve($backend, $args); + + $this->triggerPre($context, $backend, $args); + $response = $backend->deleteAll(); + $this->triggerPost($context, $response, $args); + return $response; + } + + /** + * Update a record. + * + * @param string $backend Search backend identifier + * @param RecordInterface $record The record to update + * + * @return void + */ + public function update ($backend, RecordInterface $record) + { + $context = __FUNCTION__; + $args = compact('backend', 'record', 'context'); + $backend = $this->resolve($backend, $args); + + $this->triggerPre($context, $backend, $args); + $response = $backend->update($record); + $this->triggerPost($context, $response, $args); + } + + /** + * Set application logger. + * + * @param LoggerInterface $logger Logger + * + * @return void + */ + public function setLogger (LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Set EventManager instance. + * + * @param EventManagerInterface $events Event manager + * + * @return void + */ + public function setEventManager (EventManagerInterface $events) + { + $events->setIdentifiers('VuFind\Search'); + $this->events = $events; + } + + /** + * Return EventManager instance. + * + * Lazy loads a new EventManager if none was set. + * + * @return EventManagerInterface + */ + public function getEventManager () + { + if (!$this->events) { + $this->events = new EventManager('VuFind\Search'); + } + return $this->events; + } + + /// Internal API + + /** + * Resolve a backend. + * + * @param string $backend Backend name + * @param array|ArrayAccess $args Service function arguments + * + * @return BackendInterface + * + * @throws Exception\RuntimeException Unable to resolve backend + */ + protected function resolve ($backend, $args) + { + if (!isset($this->backends[$backend])) { + $response = $this->getEventManager()->trigger( + "resolve", + $this, + $args, + function ($o) { + return ($o instanceOf BackendInterface); + } + ); + if (!$response->stopped()) { + throw new Exception\RuntimeException( + sprintf('Unable to resolve backend: %s, %s', $args['context'], $args['backend']) + ); + } + $this->backends[$backend] = $response->last(); + } + return $this->backends[$backend]; + } + + /** + * Trigger the pre event. + * + * @param string $context Service context + * @param BackendInterface $backend Selected backend + * @param array $args Event arguments + * + * @return void + */ + protected function triggerPre ($context, BackendInterface $backend, $args) + { + $this->getEventManager()->trigger($context . '.pre', $backend, $args); + } + + /** + * Trigger the post event. + * + * @param string $context Service context + * @param mixed $response Backend response + * @param array $args Event arguments + * + * @return void + */ + protected function triggerPost ($context, $response, $args) + { + $this->getEventManager()->trigger($context . '.post', $response, $args); + } + + /** + * Send a message to the logger. + * + * @param string $level Log level + * @param string $message Log message + * @param array $context Log context + * + * @return void + */ + protected function log ($level, $message, array $context = array()) + { + if ($this->logger) { + $this->logger->$level($message, $context); + } + } +} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/bootstrap.php b/module/VuFindSearch/tests/unit-tests/bootstrap.php new file mode 100644 index 00000000000..0295fb3a5b0 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/bootstrap.php @@ -0,0 +1,24 @@ +<?php + +/** + * Search subsystem PHPUnit bootstrap. + * + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @copyright Copyright (C) Villanova University 2011 + */ + +require_once('Zend/Loader/AutoloaderFactory.php'); +\Zend\Loader\AutoloaderFactory::factory( + array( + 'Zend\Loader\StandardAutoloader' => array( + 'namespaces' => array( + 'VuFindSearch' => realpath(__DIR__ . '/../../src/VuFindSearch'), + 'VuFindTest' => realpath(__DIR__ . '/src/VuFindTest'), + ), + 'autoregister_zf' => true + ) + ) +); + +define('PHPUNIT_FIXTURES', realpath(__DIR__ . '/fixtures')); \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml b/module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml new file mode 100644 index 00000000000..9f844caae2d --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml @@ -0,0 +1,495 @@ +--- +# Listing of search types and their component parts and weights. +# +# Format is: +# searchType: +# # CustomMunge is an optional section to define custom pre-processing of +# # user input. See below for details of munge actions. +# CustomMunge: +# MungeName1: +# - [action1, actionParams] +# - [action2, actionParams] +# - [action3, actionParams] +# MungeName2: +# - [action1, actionParams] +# # DismaxFields is optional and defines the fields sent to the Dismax handler +# # when we are able to use it. QueryFields will be used for advanced +# # searches that Dismax cannot support. QueryFields is always used if no +# # DismaxFields section is defined. +# DismaxFields: +# - field1^boost +# - field2^boost +# - field3^boost +# # DismaxParams is optional and allows you to override default Dismax settings +# # (i.e. mm / bf) on a search-by-search basis. If you want global default +# # values for these settings, you can edit the "dismax" search handler in +# # solr/biblio/conf/solrconfig.xml. +# DismaxParams: +# - [param1_name, param1_value] +# - [param2_name, param2_value] +# - [param3_name, param3_value] +# # QueryFields define the fields we are searching when not using Dismax +# QueryFields: +# - SolrField: +# - [howToMungeSearchstring, weight] +# - [differentMunge, weight] +# - DifferentSolrField: +# - [howToMunge, weight] +# # The optional FilterQuery section allows you to AND a static query to the +# # dynamic query generated using the QueryFields; see JournalTitle below +# # for an example. This is applied whether we use DismaxFields or +# # QueryFields. +# FilterQuery: (optional Lucene filter query) +# +# ...etc. +# +#----------------------------------------------------------------------------------- +# +# Within the QueryFields area, fields are OR'd together, unless they're in an +# anonymous array, in which case the first element is a two-value array that tells +# us what the type (AND or OR) and weight of the whole group should be. +# +# So, given: +# +# test: +# QueryFields: +# - A: +# - [onephrase, 500] +# - [and, 200] +# - B: +# - [and, 100] +# - [or, 50] +# # Start an anonymous array to group; first element indicates AND grouping +# # and a weight of 50 +# - +# - [AND, 50] +# - C: +# - [onephrase, 200] +# - D: +# - [onephrase, 300] +# # Note the "not" attached to the field name as a minus, and the use of ~ +# # to mean null ("no special weight") +# - -E: +# - [or, ~] +# - D: +# - [or, 100] +# +# ...and the search string +# +# test "one two" +# +# ...we'd get +# +# (A:"test one two"^500 OR +# A:(test AND "one two")^ 200 OR +# B:(test AND "one two")^100 OR +# B:(test OR "one two")^50 +# ( +# C:("test one two")^200 AND +# D:"test one two"^300 AND +# -E:(test OR "one two") +# )^50 OR +# D:(test OR "one two")^100 +# ) +# +#----------------------------------------------------------------------------------- +# +# Munge types are based on the original Solr.php code, and consist of: +# +# onephrase: eliminate all quotes and do it as a single phrase. +# testing "one two" +# ...becomes ("testing one two") +# +# and: AND the terms together +# testing "one two" +# ...becomes (testing AND "one two") +# +# or: OR the terms together +# testing "one two" +# ...becomes (testing OR "one two") +# +# Additional Munge types can be defined in the CustomMunge section. Each array +# entry under CustomMunge defines a new named munge type. Each array entry under +# the name of the munge type specifies a string manipulation operation. Operations +# will be applied in the order listed, and different operations take different +# numbers of parameters. +# +# Munge operations: +# +# [append, text] - Append text to the end of the user's search string +# [lowercase] - Convert string to lowercase +# [preg_replace, pattern, replacement] - Perform a regular expression replace +# using the preg_replace() PHP function. If you use backreferences in your +# replacement phrase, be sure to escape dollar signs (i.e. \$1, not $1). +# [uppercase] - Convert string to uppercase +# +# See the CallNumber search below for an example of custom munging in action. +#----------------------------------------------------------------------------------- + +# These searches use Dismax when possible: +Author: + DismaxFields: + - author^100 + - author_fuller^50 + - author2 + - author_additional + QueryFields: + - author: + - [onephrase, 350] + - [and, 200] + - [or, 100] + - author_fuller: + - [onephrase, 200] + - [and, 100] + - [or, 50] + - author2: + - [onephrase, 100] + - [and, 50] + - [or, ~] + - author_additional: + - [onephrase, 100] + - [and, 50] + - [or, ~] + +ISN: + DismaxFields: + - isbn + - issn + QueryFields: + - issn: + - [and, 100] + - [or, ~] + - isbn: + - [and, 100] + - [or, ~] + +Subject: + DismaxFields: + - topic_unstemmed^150 + - topic^100 + - geographic^50 + - genre^50 + - era + QueryFields: + - topic_unstemmed: + - [onephrase, 350] + - [and, 150] + - [or, ~] + - topic: + - [onephrase, 300] + - [and, 100] + - [or, ~] + - geographic: + - [onephrase, 300] + - [and, 100] + - [or, ~] + - genre: + - [onephrase, 300] + - [and, 100] + - [or, ~] + - era: + - [and, 100] + - [or, ~] + +# This field definition is a compromise that supports both journal-level and +# article-level data. The disadvantage is that hits in article titles will +# be mixed in. If you are building a purely article-oriented index, you should +# customize this to remove all of the title_* fields and focus entirely on the +# container_title field. +JournalTitle: + DismaxFields: + - title_short^500 + - title_full_unstemmed^450 + - title_full^400 + - title^300 + - container_title^250 + - title_alt^200 + - title_new^100 + - title_old + - series^100 + - series2 + QueryFields: + - title_short: + - [onephrase, 500] + - title_full_unstemmed: + - [onephrase, 450] + - [and, 400] + - title_full: + - [onephrase, 400] + - title: + - [onephrase, 300] + - [and, 250] + - container_title: + - [onephrase, 275] + - [and, 225] + - title_alt: + - [and, 200] + - title_new: + - [and, 100] + - title_old: + - [and, ~] + - series: + - [onephrase, 100] + - [and, 50] + - series2: + - [onephrase, 50] + - [and , ~] + FilterQuery: "format:Journal OR format:Article" + +Title: + DismaxFields: + - title_short^500 + - title_full_unstemmed^450 + - title_full^400 + - title^300 + - title_alt^200 + - title_new^100 + - title_old + - series^100 + - series2 + QueryFields: + - title_short: + - [onephrase, 500] + - title_full_unstemmed: + - [onephrase, 450] + - [and, 400] + - title_full: + - [onephrase, 400] + - title: + - [onephrase, 300] + - [and, 250] + - title_alt: + - [and, 200] + - title_new: + - [and, 100] + - title_old: + - [and, ~] + - series: + - [onephrase, 100] + - [and, 50] + - series2: + - [onephrase, 50] + - [and , ~] + +Series: + DismaxFields: + - series^100 + - series2 + QueryFields: + - series: + - [onephrase, 500] + - [and, 200] + - [or, 100] + - series2: + - [onephrase, 50] + - [and, 50] + - [or, ~] + +AllFields: + DismaxFields: + - title_short^750 + - title_full_unstemmed^600 + - title_full^400 + - title^500 + - title_alt^200 + - title_new^100 + - series^50 + - series2^30 + - author^300 + - author_fuller^150 + - contents^10 + - topic_unstemmed^550 + - topic^500 + - geographic^300 + - genre^300 + - allfields_unstemmed^10 + - fulltext_unstemmed^10 + - allfields + - fulltext + QueryFields: + 0: + - [OR, 50] + - title_short: + - [onephrase, 750] + - title_full_unstemmed: + - [onephrase, 600] + - [and, 500] + - title_full: + - [onephrase, 400] + - title: + - [onephrase, 300] + - [and, 250] + - title_alt: + - [and, 200] + - title_new: + - [and, 100] + series: + - [and, 50] + series2: + - [and, 30] + author: + - [onephrase, 300] + - [and, 250] + author_fuller: + - [onephrase, 150] + - [and, 125] + author2: + - [and, 50] + author_additional: + - [and, 50] + contents: + - [and, 10] + topic_unstemmed: + - [onephrase, 550] + - [and, 500] + topic: + - [onephrase, 500] + geographic: + - [onephrase, 300] + genre: + - [onephrase, 300] + allfields_unstemmed: + - [or, 10] + fulltext_unstemmed: + - [or, 10] + allfields: + - [or, ~] + fulltext: + - [or, ~] + +# These are advanced searches that never use Dismax: +id: + QueryFields: + - id: + - [onephrase, ~] + +# Fields for exact matches originating from alphabetic browse +ids: + QueryFields: + - id: + - [or, ~] + +TopicBrowse: + QueryFields: + - topic_browse: + - [onephrase, ~] + +AuthorBrowse: + QueryFields: + - author_browse: + - [onephrase, ~] + +TitleBrowse: + QueryFields: + - title_full: + - [onephrase, ~] + +DeweyBrowse: + QueryFields: + - dewey-raw: + - [onephrase, ~] + +LccBrowse: + QueryFields: + - callnumber-a: + - [onephrase, ~] + +CallNumber: + # We use two similar munges here -- one for exact matches, which will get + # a very high boost factor, and one for left-anchored wildcard searches, + # which will return a larger number of hits at a lower boost. + CustomMunge: + callnumber_exact: + - [uppercase] + # Strip whitespace and quotes: + - [preg_replace, '/[ "]/', ""] + # Escape colons (unescape first to avoid double-escapes): + - [preg_replace, '/(\\\:)/', ':'] + - [preg_replace, '/:/', '\:'] + # Strip pre-existing trailing asterisks: + - [preg_replace, "/\*+$/", ""] + callnumber_fuzzy: + - [uppercase] + # Strip whitespace and quotes: + - [preg_replace, '/[ "]/', ""] + # Escape colons (unescape first to avoid double-escapes): + - [preg_replace, '/(\\\:)/', ':'] + - [preg_replace, '/:/', '\:'] + # Strip pre-existing trailing asterisks: + - [preg_replace, "/\*+$/", ""] + # Ensure we have just one trailing asterisk. The trailing space inside + # the quotes has no effect on searching; it is a workaround for a + # Horde::YAML parsing glitch -- see VUFIND-160 in JIRA for details. + - [append, "* "] + QueryFields: + - callnumber: + - [callnumber_exact, 1000] + - [callnumber_fuzzy, ~] + - dewey-full: + - [callnumber_exact, 1000] + - [callnumber_fuzzy, ~] + +publisher: + DismaxFields: + - publisher^100 + QueryFields: + - publisher: + - [and, 100] + - [or, ~] + +year: + DismaxFields: + - publishDate^100 + QueryFields: + - publishDate: + - [and, 100] + - [or, ~] + +language: + QueryFields: + - language: + - [and, ~] + +toc: + DismaxFields: + - contents^100 + QueryFields: + - contents: + - [and, 100] + - [or, ~] + +topic: + QueryFields: + - topic: + - [and, 50] + - topic_facet: + - [and, ~] + +geographic: + QueryFields: + - geographic: + - [and, 50] + - geographic_facet: + - [and ~] + +genre: + QueryFields: + - genre: + - [and, 50] + - genre_facet: + - [and, ~] + +era: + QueryFields: + - era: + - [and, ~] + +oclc_num: + CustomMunge: + oclc_num: + - [preg_replace, "/[^0-9]/", ""] + # trim leading zeroes: + - [preg_replace, "/^0*/", ""] + QueryFields: + - oclc_num: + - [oclc_num, ~] diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request new file mode 100644 index 00000000000..d81a9ee6a77 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request @@ -0,0 +1 @@ +HTTP/1.1 400 Bad Request \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error new file mode 100644 index 00000000000..6432eb9f9c9 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error @@ -0,0 +1 @@ +HTTP/1.1 500 Internal Server Error \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match new file mode 100644 index 00000000000..7fc50f3b77d --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Date: Thu, 11 Oct 2012 07:56:30 GMT +Last-Modified: Thu, 11 Oct 2012 07:05:29 GMT +Server: Jetty(6.1.11) + +{"responseHeader":{"status":0,"QTime":0,"params":{"json.nl":"arrarr","wt":"json","q":"id:690xx0223"}},"response":{"numFound":0,"start":0,"docs":[]}} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record new file mode 100644 index 00000000000..c4478742955 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Date: Thu, 11 Oct 2012 07:56:30 GMT +Last-Modified: Thu, 11 Oct 2012 07:05:29 GMT +Server: Jetty(6.1.11) + +{"responseHeader":{"status":0,"QTime":0,"params":{"json.nl":"arrarr","wt":"json","q":"id:single-record"}},"response":{"numFound":1,"start":0,"docs":[{"illustrated":"Not Illustrated","id":"690250223","title":"Politics otherwise : Shakespeare as social and political critique /","title_sub":"Shakespeare as social and political critique /","spelling":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ... Amsterdam [u.a.] Rodopi 2012 XIV, 180 S. 23 cm Value inquiry book series 242 Philosophy. literature and politics Donskis, Leonidas (DE-601)391861484 (DE-601)357871030 (DE-600)14823147 Value inquiry book series 242","recordtype":"marc","title_auth":"Politics otherwise : Shakespeare as social and political critique /","title_short":"Politics otherwise :","title_sort":"politics otherwise :shakespeare as social and political critique","fullrecord":"00846nam a2200229 c 4500001001000000003000700010005001700017008004100034020002200075035002500097040002200122041000800144044001200152245010200164260003500266300002300301490007200324700004100396830007200437900005700509954005000566\u001e690250223\u001eDE-601\u001e20120419125548.0\u001e120419s2012 ne 000 0 eng d\u001e \u001fa978-90-420-3464-8\u001e \u001fa(DE-599)GBV690250223\u001e \u001faGBVCP\u001fbger\u001ferakwb\u001e0 \u001faeng\u001e \u001fane\u001faxxu\u001e10\u001faPolitics otherwise :\u001fbShakespeare as social and political critique /\u001fced. by Leonidas Donskis ...\u001e3 \u001faAmsterdam [u.a.]\u001fbRodopi\u001fc2012\u001e \u001faXIV, 180 S.\u001fc23 cm\u001e0 \u001faValue inquiry book series\u001fv242\u001faPhilosophy. literature and politics\u001e1 \u001faDonskis, Leonidas\u001f0(DE-601)391861484\u001e \u001fw(DE-601)357871030\u001fw(DE-600)14823147\u001faValue inquiry book series\u001fv242\u001e \u001faGBV\u001fbHAB Wolfenbüttel <23>\u001fd62.2354\u001fxN\u001fzN\u001fd62.2354\u001e \u001fa50\u001fb1305716574\u001fc01\u001fd62.2354\u001fea\u001fd62.2354\u001fx0023\u001e\u001d","title_full":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ...","title_fullStr":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ...","title_full_unstemmed":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ...","series":["Value inquiry book series"],"physical":["XIV, 180 S. 23 cm"],"building":["Library A"],"collection":["Catalog"],"format":["Book"],"series2":["Value inquiry book series","Philosophy. literature and politics"],"publisher":["Rodopi"],"ctrlnum":["(DE-599)GBV690250223"],"spellingShingle":["Value inquiry book series","Politics otherwise : Shakespeare as social and political critique /"],"isbn":["978-90-420-3464-8"],"author2Str":["Donskis, Leonidas"],"author2":["Donskis, Leonidas"],"language":["English"],"publishDate":["2012"],"institution":["MyInstitution"]}]}} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/phpunit.xml b/module/VuFindSearch/tests/unit-tests/phpunit.xml new file mode 100644 index 00000000000..fe5b7573e41 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/phpunit.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<phpunit bootstrap="bootstrap.php" + strict="true"> + <testsuites> + <testsuite name="VuFindSearch"> + <directory>src</directory> + </testsuite> + </testsuites> + <php> + <includePath>../../vendor/zf2/library</includePath> + </php> +</phpunit> diff --git a/module/VuFindSearch/tests/unit-tests/src/.keep b/module/VuFindSearch/tests/unit-tests/src/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php new file mode 100644 index 00000000000..93634163335 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php @@ -0,0 +1,88 @@ +<?php + +/** + * Unit tests for SOLR backend. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindTest\Backend\Solr; + +use VuFindSearch\Backend\Solr\Backend; +use Zend\Http\Response; +use PHPUnit_Framework_TestCase; +use InvalidArgumentException; + +/** + * Unit tests for SOLR backend. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class BackendTest extends PHPUnit_Framework_TestCase +{ + /** + * Test retrieving a record. + * + * @return void + */ + public function testRetrieve () + { + $resp = $this->loadResponse('single-record'); + $conn = $this->getMock('VuFindSearch\Backend\Solr\Connector', array('retrieve'), array('http://example.tld/')); + $conn->expects($this->once()) + ->method('retrieve') + ->will($this->returnValue($resp->getBody())); + + $back = new Backend('test', $conn); + $coll = $back->retrieve('foobar'); + $this->assertCount(1, $coll); + $this->assertEquals('test', $coll->getSourceIdentifier()); + $rec = $coll->first(); + $this->assertEquals('test', $rec->getSourceIdentifier()); + $this->assertEquals('690250223', $rec->id); + } + + /** + * Load a SOLR response as fixture. + * + * @param string $fixture Fixture file + * + * @return Zend\Http\Response + * + * @throws InvalidArgumentException Fixture files does not exist + */ + protected function loadResponse ($fixture) + { + $file = realpath(sprintf('%s/solr/response/%s', PHPUNIT_FIXTURES, $fixture)); + if (!is_string($file) || !file_exists($file) || !is_readable($file)) { + throw new InvalidArgumentException(sprintf('Unable to load fixture file: %s', $file)); + } + return Response::fromString(file_get_contents($file)); + } + +} diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php new file mode 100644 index 00000000000..4a1d38496cb --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php @@ -0,0 +1,143 @@ +<?php + +/** + * Unit tests for SOLR connector. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindTest\Backend\Solr; + +use VuFindSearch\Backend\Solr\Connector; + +use Zend\Http\Client\Adapter\Test as TestAdapter; +use Zend\Http\Client as HttpClient; + +use PHPUnit_Framework_TestCase; +use InvalidArgumentException; + +/** + * Unit tests for SOLR connector. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class ConnectorTest extends PHPUnit_Framework_TestCase +{ + /** + * Current response. + * + * @var string + */ + protected $response; + + /** + * Test record retrieval. + * + * @return void + */ + public function testRetrieve () + { + $conn = $this->createConnector('single-record'); + $resp = $conn->retrieve('id'); + $this->assertInternalType('string', $resp); + json_decode($resp, true); + $this->assertEquals(\JSON_ERROR_NONE, json_last_error()); + } + + /** + * Test retrieving a non-existent record returns a response. + * + * @return void + */ + public function testRetrieveMissingRecord () + { + $conn = $this->createConnector('no-match'); + $resp = $conn->retrieve('id'); + $this->assertInternalType('string', $resp); + } + + /** + * Test RemoteErrorException is thrown on a remote 5xx error. + * + * @expectedException VuFindSearch\Backend\Exception\RemoteErrorException + * @expectedExceptionCode 500 + */ + public function testInternalServerError () + { + $conn = $this->createConnector('internal-server-error'); + $resp = $conn->retrieve('id'); + } + + /** + * Test RequestErrorException is thrown on a remote 4xx error. + * + * @expectedException VuFindSearch\Backend\Exception\RequestErrorException + * @expectedExceptionCode 400 + */ + public function testBadRequestError () + { + $conn = $this->createConnector('bad-request'); + $resp = $conn->retrieve('id'); + } + + /** + * Create connector with fixture file. + * + * @param string $fixture Fixture file + * + * @return Connector + * + * @throws InvalidArgumentException Fixture file does not exist + */ + protected function createConnector ($fixture) + { + $file = realpath(sprintf('%s/solr/response/%s', PHPUNIT_FIXTURES, $fixture)); + if (!is_string($file) || !file_exists($file) || !is_readable($file)) { + throw new InvalidArgumentException(sprintf('Unable to load fixture file: %s', $file)); + } + $this->response = file_get_contents($file); + + $conn = new Connector('http://example.tld/'); + $conn->setProxy($this); + return $conn; + } + + /** + * Set test adapter with prepared response. + * + * @param HttpClient $client HTTP client to mock + * + * @return void + */ + public function proxify (HttpClient $client) + { + $adapter = new TestAdapter(); + $adapter->setResponse($this->response); + $client->setAdapter($adapter); + } +} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php new file mode 100644 index 00000000000..0169a9b231f --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php @@ -0,0 +1,148 @@ +<?php + +/** + * Unit tests for SOLR query builder + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindTest\Backend\Solr; + +use VuFindSearch\Backend\Solr\QueryBuilder; +use PHPUnit_Framework_TestCase; + +/** + * Unit tests for SOLR query builder + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class QueryBuilderTest extends PHPUnit_Framework_TestCase +{ + /** + * Test capitalizeBooleans functionality. + * + * @return void + */ + public function testCapitalizeBooleans() + { + $qb = new QueryBuilder(); + + // Set up an array of expected inputs and outputs: + // @codingStandardsIgnoreStart + $tests = array( + array('this not that', 'this NOT that'), // capitalize not + array('this and that', 'this AND that'), // capitalize and + array('this or that', 'this OR that'), // capitalize or + array('apples and oranges (not that)', 'apples AND oranges (NOT that)'), + array('"this not that"', '"this not that"'), // do not capitalize inside quotes + array('"this and that"', '"this and that"'), // do not capitalize inside quotes + array('"this or that"', '"this or that"'), // do not capitalize inside quotes + array('"apples and oranges (not that)"', '"apples and oranges (not that)"'), + array('this AND that', 'this AND that'), // don't mess up existing caps + array('and and and', 'and AND and'), + array('andornot noted andy oranges', 'andornot noted andy oranges'), + array('(this or that) and (apples not oranges)', '(this OR that) AND (apples NOT oranges)'), + array('this aNd that', 'this AND that'), // strange capitalization of AND + array('this nOt that', 'this NOT that') // strange capitalization of NOT + ); + // @codingStandardsIgnoreEnd + + // Test all the operations: + foreach ($tests as $current) { + $this->assertEquals( + $qb->capitalizeBooleans($current[0]), $current[1] + ); + } + } + + /** + * Test capitalizeRanges functionality. + * + * @return void + */ + public function testCapitalizeRanges() + { + $qb = new QueryBuilder(); + + // Set up an array of expected inputs and outputs: + // @codingStandardsIgnoreStart + $tests = array( + array('"{a to b}"', '"{a to b}"'), // don't capitalize inside quotes + array('"[a to b]"', '"[a to b]"'), + array('[a to b]', '([a TO b] OR [A TO B])'), // expand alphabetic cases + array('[a TO b]', '([a TO b] OR [A TO B])'), + array('[a To b]', '([a TO b] OR [A TO B])'), + array('[a tO b]', '([a TO b] OR [A TO B])'), + array('{a to b}', '({a TO b} OR {A TO B})'), + array('{a TO b}', '({a TO b} OR {A TO B})'), + array('{a To b}', '({a TO b} OR {A TO B})'), + array('{a tO b}', '({a TO b} OR {A TO B})'), + array('[1900 to 1910]', '[1900 TO 1910]'), // don't expand numeric cases + array('[1900 TO 1910]', '[1900 TO 1910]'), + array('{1900 to 1910}', '{1900 TO 1910}'), + array('{1900 TO 1910}', '{1900 TO 1910}'), + array('[a to b]', '([a TO b] OR [A TO B])'), // handle extra spaces + // special case for timestamps: + array('[1900-01-01t00:00:00z to 1900-12-31t23:59:59z]', '[1900-01-01T00:00:00Z TO 1900-12-31T23:59:59Z]'), + array('{1900-01-01T00:00:00Z TO 1900-12-31T23:59:59Z}', '{1900-01-01T00:00:00Z TO 1900-12-31T23:59:59Z}') + ); + // @codingStandardsIgnoreEnd + + // Test all the operations: + foreach ($tests as $current) { + $this->assertEquals( + $qb->capitalizeRanges($current[0]), $current[1] + ); + } + } + + /** + * Test parseRange functionality. + * + * @return void + */ + public function testParseRange() + { + $this->markTestSkipped(); + $qb = new QueryBuilder(); + + // basic range test: + $result = $qb->parseRange("[1 TO 100]"); + $this->assertEquals('1', $result['from']); + $this->assertEquals('100', $result['to']); + + // test whitespace handling: + $result = $qb->parseRange("[1 TO 100]"); + $this->assertEquals('1', $result['from']); + $this->assertEquals('100', $result['to']); + + // test invalid ranges: + $this->assertFalse($qb->parseRange('1 TO 100')); + $this->assertFalse($qb->parseRange('[not a range to me]')); + } +} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php new file mode 100644 index 00000000000..45198429e26 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php @@ -0,0 +1,58 @@ +<?php + +/** + * Unit tests for simple JSON-based record collection factory. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindTest\Backend\Solr\Json\Response; + +use VuFindSearch\Backend\Solr\Response\Json\RecordCollectionFactory; +use PHPUnit_Framework_TestCase; + +/** + * Unit tests for simple JSON-based record collection factory. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class RecordCollectionFactoryTest extends PHPUnit_Framework_TestCase +{ + /** + * Test that the factory creates a collection. + * + * @return void + */ + public function testFactory () + { + $json = json_encode(array('response' => array('start' => 0, 'docs' => array(array(), array(), array())))); + $fact = new RecordCollectionFactory(); + $coll = $fact->factory(json_decode($json, true)); + $this->assertEquals(3, count($coll)); + } +} \ No newline at end of file diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php new file mode 100644 index 00000000000..0e5e02c07b3 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php @@ -0,0 +1,57 @@ +<?php + +/** + * Unit tests for SOLR search handler. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2010. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ + +namespace VuFindTest\Backend\Solr; + +use VuFindSearch\Backend\Solr\SearchHandler; +use PHPUnit_Framework_TestCase; + +/** + * Unit tests for SOLR search handler. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org + */ +class SearchHandlerTest extends PHPUnit_Framework_TestCase +{ + /** + * Test creating simple dismax query. + * + * @return void + */ + public function testSimpleSearchDismax () + { + $spec = array('DismaxParams' => array(array('foo', 'bar')), 'DismaxFields' => array('field1', 'field2')); + $hndl = new SearchHandler($spec); + $this->assertEquals('(_query_:"{!dismax qf=\"field1 field2\" foo=\\\'bar\\\'}foobar")', $hndl->createSimpleQueryString('foobar')); + } +} -- GitLab