5adff6df3f486496f35fcde46035088bfb13fdba
[moodle.git] / search / engine / solr / classes / schema.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Solr schema manipulation manager.
19  *
20  * @package   search_solr
21  * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace search_solr;
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/lib/filelib.php');
31 /**
32  * Schema class to interact with Solr schema.
33  *
34  * At the moment it only implements create which should be enough for a basic
35  * moodle configuration in Solr.
36  *
37  * @package   search_solr
38  * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
39  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class schema {
43     /**
44      * @var stdClass
45      */
46     protected $config = null;
48     /**
49      * cUrl instance.
50      * @var \curl
51      */
52     protected $curl = null;
54     /**
55      * The URL.
56      * @var string
57      */
58     protected $url = null;
60     /**
61      * The schema URL.
62      * @var string
63      */
64     protected $schemaurl = null;
66     /**
67      * Constructor.
68      *
69      * @throws \moodle_exception
70      * @return void
71      */
72     public function __construct() {
73         if (!$this->config = get_config('search_solr')) {
74             throw new \moodle_exception('missingconfig', 'search_solr');
75         }
77         if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
78             throw new \moodle_exception('missingconfig', 'search_solr');
79         }
81         $this->curl = new \curl();
83         // HTTP headers.
84         $this->curl->setHeader('Content-type: application/json');
85         if (!empty($this->config->server_username) && !empty($this->config->server_password)) {
86             $authorization = $this->config->server_username . ':' . $this->config->server_password;
87             $this->curl->setHeader('Authorization', 'Basic ' . base64_encode($authorization));
88         }
90         $this->url = rtrim($this->config->server_hostname, '/');
91         if (!empty($this->config->server_port)) {
92             $this->url .= ':' . $this->config->server_port;
93         }
94         $this->url .= '/solr/' . $this->config->indexname;
95         $this->schemaurl = $this->url . '/schema';
98     }
100     /**
101      * Setup solr stuff required by moodle.
102      *
103      * @param  bool $checkexisting Whether to check if the fields already exist or not
104      * @return bool
105      */
106     public function setup($checkexisting = true) {
107         $fields = \search_solr\document::get_default_fields_definition();
109         // Field id is already there.
110         unset($fields['id']);
112         $this->check_index();
114         return $this->add_fields($fields, $checkexisting);
115     }
117     /**
118      * Checks the schema is properly set up.
119      *
120      * @throws \moodle_exception
121      * @return void
122      */
123     public function validate_setup() {
124         $fields = \search_solr\document::get_default_fields_definition();
126         // Field id is already there.
127         unset($fields['id']);
129         $this->check_index();
130         $this->validate_fields($fields, true);
131     }
133     /**
134      * Checks if the index is ready, triggers an exception otherwise.
135      *
136      * @throws \moodle_exception
137      * @return void
138      */
139     protected function check_index() {
141         // Check that the server is available and the index exists.
142         $result = $this->curl->get($this->url . '/select?wt=json');
143         if ($this->curl->error) {
144             throw new \moodle_exception('connectionerror', 'search_solr');
145         }
146         if ($this->curl->info['http_code'] === 404) {
147             throw new \moodle_exception('connectionerror', 'search_solr');
148         }
149     }
151     /**
152      * Adds the provided fields to Solr schema.
153      *
154      * Intentionally separated from create(), it can be called to add extra fields.
155      * fields separately.
156      *
157      * @throws \coding_exception
158      * @throws \moodle_exception
159      * @param  array $fields \core_search\document::$requiredfields format
160      * @param  bool $checkexisting Whether to check if the fields already exist or not
161      * @return bool
162      */
163     protected function add_fields($fields, $checkexisting = true) {
165         if ($checkexisting) {
166             // Check that non of them exists.
167             $this->validate_fields($fields, false);
168         }
170         // Add all fields.
171         foreach ($fields as $fieldname => $data) {
173             if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) {
174                 throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.');
175             }
176             // Changing default multiValued value to false as we want to match values easily.
177             $params = array(
178                 'add-field' => array(
179                     'name' => $fieldname,
180                     'type' => $data['type'],
181                     'stored' => $data['stored'],
182                     'multiValued' => false,
183                     'indexed' => $data['indexed']
184                 )
185             );
186             $results = $this->curl->post($this->schemaurl, json_encode($params));
188             // We only validate if we are interested on it.
189             if ($checkexisting) {
190                 if ($this->curl->error) {
191                     throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
192                 }
193                 $this->validate_add_field_result($results);
194             }
195         }
197         return true;
198     }
200     /**
201      * Checks if the schema existing fields are properly set, triggers an exception otherwise.
202      *
203      * @throws \moodle_exception
204      * @param array $fields
205      * @param bool $requireexisting Require the fields to exist, otherwise exception.
206      * @return void
207      */
208     protected function validate_fields(&$fields, $requireexisting = false) {
209         global $CFG;
211         foreach ($fields as $fieldname => $data) {
212             $results = $this->curl->get($this->schemaurl . '/fields/' . $fieldname);
214             if ($this->curl->error) {
215                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
216             }
218             if (!$results) {
219                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
220             }
221             $results = json_decode($results);
223             if ($requireexisting && !empty($results->error) && $results->error->code === 404) {
224                 $a = new \stdClass();
225                 $a->fieldname = $fieldname;
226                 $a->setupurl = $CFG->wwwroot . '/search/engine/solr/setup_schema.php';
227                 throw new \moodle_exception('errorvalidatingschema', 'search_solr', '', $a);
228             }
230             // The field should not exist so we only accept 404 errors.
231             if (empty($results->error) || (!empty($results->error) && $results->error->code !== 404)) {
232                 if (!empty($results->error)) {
233                     throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error->msg);
234                 } else {
235                     // All these field attributes are set when fields are added through this script and should
236                     // be returned and match the defined field's values.
238                     if (empty($results->field) || !isset($results->field->type) ||
239                             !isset($results->field->multiValued) || !isset($results->field->indexed) ||
240                             !isset($results->field->stored)) {
242                         throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
243                             get_string('schemafieldautocreated', 'search_solr', $fieldname));
245                     } else if ($results->field->type !== $data['type'] ||
246                                 $results->field->multiValued !== false ||
247                                 $results->field->indexed !== $data['indexed'] ||
248                                 $results->field->stored !== $data['stored']) {
250                             throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
251                                 get_string('schemafieldautocreated', 'search_solr', $fieldname));
252                     } else {
253                         // The field already exists and it is properly defined, no need to create it.
254                         unset($fields[$fieldname]);
255                     }
256                 }
257             }
258         }
259     }
261     /**
262      * Checks that the field results do not contain errors.
263      *
264      * @throws \moodle_exception
265      * @param string $results curl response body
266      * @return void
267      */
268     protected function validate_add_field_result($result) {
270         if (!$result) {
271             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
272         }
274         $results = json_decode($result);
275         if (!$results) {
276             if (is_scalar($result)) {
277                 $errormsg = $result;
278             } else {
279                 $errormsg = json_encode($result);
280             }
281             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errormsg);
282         }
284         // It comes as error when fetching fields data.
285         if (!empty($results->error)) {
286             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error);
287         }
289         // It comes as errors when adding fields.
290         if (!empty($results->errors)) {
292             // We treat this error separately.
293             $errorstr = '';
294             foreach ($results->errors as $error) {
295                 $errorstr .= implode(', ', $error->errorMessages);
296             }
297             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errorstr);
298         }
300     }