1 <?php
2 /*
3 Copyright (c) 2012, University of Cambridge Computing Service
4
5 This file is part of the Lookup/Ibis client library.
6
7 This library is free software: you can redistribute it and/or modify
8 it under the terms of the GNU Lesser General Public License as published
9 by the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
15 License for more details.
16
17 You should have received a copy of the GNU Lesser General Public License
18 along with this library. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 require_once "IbisAttribute.php";
22 require_once "IbisAttributeScheme.php";
23 require_once "IbisContactPhoneNumber.php";
24 require_once "IbisContactRow.php";
25 require_once "IbisContactWebPage.php";
26 require_once "IbisDto.php";
27 require_once "IbisError.php";
28 require_once "IbisGroup.php";
29 require_once "IbisIdentifier.php";
30 require_once "IbisInstitution.php";
31 require_once "IbisPerson.php";
32
33 /**
34 * Class representing the top-level container for all XML and JSON results.
35 * This may be just a simple textual value or it may contain more complex
36 * entities such as people, institutions, groups, attributes, etc.
37 *
38 * @author Dean Rasheed (dev-group@ucs.cam.ac.uk)
39 */
40 class IbisResult extends IbisDto
41 {
42 /* Properties marked as @XmlAttribte in the JAXB class */
43 protected static $xmlAttrs = array("version");
44
45 /* Properties marked as @XmlElement in the JAXB class */
46 protected static $xmlElems = array("value", "person", "institution",
47 "group", "identifier", "attribute",
48 "error", "entities");
49
50 /* Properties marked as @XmlElementWrapper in the JAXB class */
51 protected static $xmlArrays = array("people", "institutions", "groups",
52 "attributes", "attributeSchemes");
53
54 /** @var string The web service API version number. */
55 public $version;
56
57 /**
58 * @var string The value returned by methods that return a simple textual
59 * value.
60 */
61 public $value;
62
63 /**
64 * @var IbisPerson The person returned by methods that return a single
65 * person.
66 *
67 * Note that methods that may return multiple people will always use
68 * the {@link people} field, even if only one person was returned.
69 */
70 public $person;
71
72 /**
73 * @var IbisInstitution The institution returned by methods that return a
74 * single institution.
75 *
76 * Note that methods that may return multiple institutions will always
77 * use the {@link institutions} field, even if only one institution
78 * was returned.
79 */
80 public $institution;
81
82 /**
83 * @var IbisGroup The group returned by methods that return a single
84 * group.
85 *
86 * Note that methods that may return multiple groups will always use
87 * the {@link groups} field, even if only one group was returned.
88 */
89 public $group;
90
91 /**
92 * @var IbisIdentifier The identifier returned by methods that return a
93 * single identifier.
94 */
95 public $identifier;
96
97 /**
98 * @var IbisAttribute The person or institution attribute returned by
99 * methods that return a single attribute.
100 */
101 public $attribute;
102
103 /** @var IbisError If the method failed, details of the error. */
104 public $error;
105
106 /**
107 * @var IbisPerson[] The list of people returned by methods that may
108 * return multiple people. This may be empty, or contain one or more
109 * people.
110 */
111 public $people;
112
113 /**
114 * @var IbisInstitution[] The list of institutions returned by methods
115 * that may return multiple institutions. This may be empty, or contain
116 * one or more institutions.
117 */
118 public $institutions;
119
120 /**
121 * @var IbisGroup[] The list of groups returned by methods that may
122 * return multiple groups. This may be empty, or contain one or more
123 * groups.
124 */
125 public $groups;
126
127 /**
128 * @var IbisAttribute[] The list of attributes returned by methods that
129 * return lists of person/institution attributes.
130 */
131 public $attributes;
132
133 /**
134 * @var IbisAttributeScheme[] The list of attribute schemes returned by
135 * methods that return lists of person/institution attribute schemes.
136 */
137 public $attributeSchemes;
138
139 /**
140 * @ignore
141 * @var IbisResultEntities In the flattened XML/JSON representation, all
142 * the unique entities returned by the method.
143 *
144 * NOTE: This will be ``null`` unless the "flatten" parameter is
145 * ``true``.
146 */
147 public $entities;
148
149 /**
150 * @ignore
151 * Unflatten this IbisResult object, resolving any internal ID refs
152 * to build a fully fledged object tree.
153 *
154 * This is necessary if the IbisResult was constructed from XML/JSON in
155 * its flattened representation (with the "flatten" parameter set to
156 * ``true``).
157 *
158 * On entry, the IbisResult object may have people, institutions or
159 * groups in it with "ref" fields referring to objects held in the
160 * "entities" lists. After unflattening, all such references will have
161 * been replaced by actual object references, giving an object tree that
162 * can be traversed normally.
163 *
164 * @return IbisResult This IbisResult object, with its internals
165 * unflattened.
166 */
167 public function unflatten()
168 {
169 if (isset($this->entities))
170 {
171 $em = new IbisResultEntityMap($this);
172
173 if (isset($this->person))
174 $this->person = $this->person->unflatten($em);
175 if (isset($this->institution))
176 $this->institution = $this->institution->unflatten($em);
177 if (isset($this->group))
178 $this->group = $this->group->unflatten($em);
179
180 IbisPerson::unflattenPeople($em, $this->people);
181 IbisInstitution::unflattenInsts($em, $this->institutions);
182 IbisGroup::unflattenGroups($em, $this->groups);
183 }
184 return $this;
185 }
186 }
187
188 /**
189 * @ignore
190 * Class to hold the full details of all the entities returned in a result
191 * (a nested class in Java and Python). This is used only in the flattened
192 * result representation, where each of these entities will have a unique
193 * textual ID, and be referred to from the top-level objects returned (and
194 * by each other).
195 *
196 * In the hierarchical representation, this is not used, since all entities
197 * returned will be at the top-level, or directly contained in those
198 * top-level entities.
199 */
200 class IbisResultEntities extends IbisDto
201 {
202 /* Properties marked as @XmlElementWrapper in the JAXB class */
203 protected static $xmlArrays = array("people", "institutions", "groups");
204
205 /**
206 * @var IbisPerson[] A list of all the unique people returned by the
207 * method. This may include additional people returned as a result of the
208 * ``fetch`` parameter, so this list may contain more entries than the
209 * corresponding field on the enclosing class.
210 */
211 public $people;
212
213 /**
214 * @var IbisInstitution[] A list of all the unique institutions returned
215 * by the method. This may include additional institutions returned as a
216 * result of the ``fetch`` parameter, so this list may contain more
217 * entries than the corresponding field on the enclosing class.
218 */
219 public $institutions;
220
221 /**
222 * @var IbisGroup[] A list of all the unique groups returned by the
223 * method. This may include additional groups returned as a result of the
224 * ``fetch`` parameter, so this list may contain more entries than the
225 * corresponding field on the enclosing class.
226 */
227 public $groups;
228 }
229
230 /**
231 * @ignore
232 * Class to assist during the unflattening process, maintaining efficient
233 * maps from IDs to entities (people, institutions and groups). This is a
234 * nested class of IbisResult in Java and Python.
235 */
236 class IbisResultEntityMap
237 {
238 private $peopleById;
239 private $instsById;
240 private $groupsById;
241
242 /** Construct an entity map from a flattened IbisResult. */
243 public function __construct($result)
244 {
245 $this->peopleById = array();
246 $this->instsById = array();
247 $this->groupsById = array();
248
249 if (isset($result->entities->people))
250 foreach ($result->entities->people as $person)
251 $this->peopleById[$person->id] = $person;
252 if (isset($result->entities->institutions))
253 foreach ($result->entities->institutions as $inst)
254 $this->instsById[$inst->id] = $inst;
255 if (isset($result->entities->groups))
256 foreach ($result->entities->groups as $group)
257 $this->groupsById[$group->id] = $group;
258 }
259
260 /** Get a person from the entity map, given their ID */
261 public function getPerson($id) { return $this->peopleById[$id]; }
262
263 /** Get an institution from the entity map, given its ID */
264 public function getInstitution($id) { return $this->instsById[$id]; }
265
266 /** Get a group from the entity map, given its ID */
267 public function getGroup($id) { return $this->groupsById[$id]; }
268 }
269
270 /**
271 * @ignore
272 * Class to hold a XML text node's value during XML parsing.
273 */
274 class XmlTextNode
275 {
276 public $tagname;
277 public $data;
278 }
279
280 /**
281 * Class to parse the XML from the server and produce an IbisResult.
282 */
283 class IbisResultParser
284 {
285 /** The IbisResult produced from the XML */
286 private $result;
287
288 /** Stack of nodes during XML parsing */
289 private $nodeStack;
290
291 /** @ignore Start element callback function for XML parsing */
292 public function startElement($parser, $tagname, $attrs)
293 {
294 $element = null;
295 if (!empty($this->nodeStack))
296 {
297 if ($tagname === "person")
298 $element = new IbisPerson($attrs);
299 elseif ($tagname === "institution")
300 $element = new IbisInstitution($attrs);
301 elseif ($tagname === "membersOfInst")
302 $element = new IbisInstitution($attrs);
303 elseif ($tagname === "group")
304 $element = new IbisGroup($attrs);
305 elseif ($tagname === "identifier")
306 $element = new IbisIdentifier($attrs);
307 elseif ($tagname === "attribute")
308 $element = new IbisAttribute($attrs);
309 elseif ($tagname === "error")
310 $element = new IbisError($attrs);
311 elseif ($tagname === "attributeScheme")
312 $element = new IbisAttributeScheme($attrs);
313 elseif ($tagname === "contactRow")
314 $element = new IbisContactRow($attrs);
315 elseif ($tagname === "phoneNumber")
316 $element = new IbisContactPhoneNumber($attrs);
317 elseif ($tagname === "webPage")
318 $element = new IbisContactWebPage($attrs);
319 elseif ($tagname === "entities")
320 $element = new IbisResultEntities($attrs);
321 else
322 {
323 $parent = end($this->nodeStack);
324 if (!is_array($parent))
325 // Need a reference to the parent's child array
326 $element = &$parent->startChildElement($tagname);
327 }
328
329 if (is_null($element))
330 {
331 $element = new XmlTextNode();
332 $element->tagname = $tagname;
333 }
334 }
335 elseif ($tagname !== "result")
336 throw new Exception("Invalid root element: '" . $tagname . "'");
337 else
338 {
339 $element = new IbisResult($attrs);
340 $this->result = $element;
341 }
342
343 // Stack the new element. If it is an array, we must stack a
344 // reference to it that we can modify.
345 if (is_array($element)) $this->nodeStack[] = &$element;
346 else $this->nodeStack[] = $element;
347 }
348
349 /** @ignore End element callback function for XML parsing */
350 public function endElement($parser, $tagname)
351 {
352 if (!empty($this->nodeStack))
353 {
354 $element = array_pop($this->nodeStack);
355 if (!empty($this->nodeStack))
356 {
357 if (is_array(end($this->nodeStack)))
358 {
359 // Add the child to the parent's child array, which
360 // means that we must use an array reference
361 $parent = &$this->nodeStack[sizeof($this->nodeStack)-1];
362 $parent[] = $element instanceof XmlTextNode ?
363 $element->data : $element;
364 }
365 elseif (!(end($this->nodeStack) instanceof XmlTextNode))
366 {
367 $parent = end($this->nodeStack);
368 $parent->endChildElement($tagname,
369 $element instanceof XmlTextNode ?
370 $element->data : $element);
371 }
372 }
373 }
374 else
375 throw new Exception("Unexpected closing tag: '" . $tagname . "'");
376 }
377
378 /** @ignore Character data callback function for XML parsing */
379 public function charData($parser, $data)
380 {
381 if (!empty($this->nodeStack))
382 {
383 $element = end($this->nodeStack);
384 if ($element instanceof IbisIdentifier)
385 {
386 if (isset($element->value)) $element->value .= $data;
387 else $element->value = $data;
388 }
389 elseif ($element instanceof XmlTextNode)
390 {
391 if (isset($element->data)) $element->data .= $data;
392 else $element->data = $data;
393 }
394 }
395 }
396
397 /**
398 * Parse XML data from the specified string and return an IbisResult.
399 *
400 * @param string $data The XML string returned from the server.
401 *
402 * @return IbisResult The parsed results. This may contain lists or trees
403 * of objects representing people, institutions and groups returned from
404 * the server.
405 */
406 public function parseXml($data)
407 {
408 $parser = xml_parser_create();
409 xml_set_object($parser, $this);
410 xml_set_element_handler($parser, "startElement", "endElement");
411 xml_set_character_data_handler($parser, "charData");
412 xml_parser_set_option ($parser, XML_OPTION_CASE_FOLDING, false);
413
414 $this->result = null;
415 $this->nodeStack = array();
416
417 xml_parse($parser, $data);
418 xml_parser_free($parser);
419
420 return $this->result->unflatten();
421 }
422
423 /**
424 * Parse XML data from the specified stream and return an IbisResult.
425 *
426 * @param resource $file A file pointer to a stream containing XML
427 * returned from the server.
428 *
429 * @return IbisResult The parsed results. This may contain lists or trees
430 * of objects representing people, institutions and groups returned from
431 * the server.
432 */
433 public function parseXmlFile($file)
434 {
435 $parser = xml_parser_create();
436 xml_set_object($parser, $this);
437 xml_set_element_handler($parser, "startElement", "endElement");
438 xml_set_character_data_handler($parser, "charData");
439 xml_parser_set_option ($parser, XML_OPTION_CASE_FOLDING, false);
440
441 $this->result = null;
442 $this->nodeStack = array();
443
444 while ($data = fread($file, 4096))
445 xml_parse($parser, $data, feof($file));
446 xml_parser_free($parser);
447
448 return $this->result->unflatten();
449 }
450 }
451