RESTController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
12 
21 
22 /**
23  * RESTController handles REST requests.
24  *
25  * The controller supports the following actions:
26  *
27  * <div class="controller-action">
28  * <div> __Action__ _default_ </div>
29  * <div>
30  * Handle action according to HTTP method and parameters.
31  *
32  * For details about the paramters, see documentation for the methods
33  * RESTController::handleGet(), RESTController::handlePost(),
34  * RESTController::handlePut(), RESTController::handleDelete()
35  *
36  * | __Response Actions__ | |
37  * |------------------------|-------------------------
38  * | `ok` | In all cases
39  * </div>
40  * </div>
41  *
42  * @author ingo herwig <ingo@wemove.com>
43  */
44 class RESTController extends Controller {
45 
46  /**
47  * @see Controller::initialize()
48  */
49  public function initialize(Request $request, Response $response) {
50  // construct oid from className and id
51  if ($request->hasValue('className') && $request->hasValue('id')) {
52  $oid = new ObjectId($request->getValue('className'), $request->getValue('id'));
53  $request->setValue('oid', $oid->__toString());
54  }
55  // construct sourceOid from className and sourceId
56  if ($request->hasValue('className') && $request->hasValue('sourceId')) {
57  $sourceOid = new ObjectId($request->getValue('className'), $request->getValue('sourceId'));
58  $request->setValue('sourceOid', $sourceOid->__toString());
59  }
60  // construct oid, targetOid from sourceOid, relation and targetId
61  if ($request->hasValue('sourceOid') && $request->hasValue('relation') && $request->hasValue('targetId')) {
62  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
63  $relatedType = $this->getRelatedType($sourceOid, $request->getValue('relation'));
64  $targetOid = new ObjectId($relatedType, $request->getValue('targetId'));
65  $request->setValue('targetOid', $targetOid->__toString());
66  // non-collection requests
67  $request->setValue('oid', $targetOid->__toString());
68  }
69  parent::initialize($request, $response);
70  }
71 
72  /**
73  * @see Controller::validate()
74  */
75  protected function validate() {
76  $request = $this->getRequest();
77  $response = $this->getResponse();
78  if ($request->hasValue('className') &&
79  !$this->getPersistenceFacade()->isKnownType($request->getValue('className')))
80  {
81  $response->addError(ApplicationError::get('PARAMETER_INVALID',
82  array('invalidParameters' => array('className'))));
83  return false;
84  }
85  if ($request->hasHeader('Position')) {
86  $position = $request->getHeader('Position');
87  if (!preg_match('/^before /', $position) && !preg_match('/^last$/', $position)) {
88  $response->addError(ApplicationError::get('PARAMETER_INVALID',
89  array('invalidParameters' => array('Position'))));
90  return false;
91  }
92  }
93  // do default validation
94  return parent::validate();
95  }
96 
97  /**
98  * @see Controller::doExecute()
99  */
100  protected function doExecute() {
101  $request = $this->getRequest();
102  $response = $this->getResponse();
103  switch ($request->getMethod()) {
104  case 'GET':
105  $this->handleGet();
106  break;
107  case 'POST':
108  $this->handlePost();
109  break;
110  case 'PUT':
111  $this->handlePut();
112  break;
113  case 'DELETE':
114  $this->handleDelete();
115  break;
116  default:
117  $this->handleGet();
118  break;
119  }
120  $response->setAction('ok');
121  }
122 
123  /**
124  * Handle a GET request (read object(s) of a given type)
125  *
126  * | Parameter | Description
127  * |------------------|-------------------------
128  * | _in_ `collection`| Boolean whether to load one object or a list of objects. The _Range_ header is used to get only part of the list
129  * | _in_ `language` | The language of the returned object(s)
130  * | _in_ `className` | The type of returned object(s)
131  * | _in_ `id` | If collection is _false_, the object with _className_/_id_ will be loaded
132  * | _in_ `sortBy` | _?sortBy=+foo_ for sorting the list by foo ascending or
133  * | _in_ `sort` | _?sort(+foo)_ for sorting the list by foo ascending
134  * | _in_ `limit` | _?limit(10,25)_ for loading 25 objects starting from position 10
135  * | _in_ `relation` | Relation name if objects in relation to another object should be loaded (determines the type of the returned objects)
136  * | _in_ `sourceId` | Id of the object to which the returned objects are related (determines the object id together with _className_)
137  * | _out_ | Single object or list of objects. In case of a list, the _Content-Range_ header will be set.
138  *
139  */
140  protected function handleGet() {
141  $request = $this->getRequest();
142  $response = $this->getResponse();
143  $persistenceFacade = $this->getPersistenceFacade();
144 
145  if ($request->getBooleanValue('collection') === false) {
146  // read a specific object
147  // delegate further processing to DisplayController
148 
149  // execute action
150  $subResponse = $this->executeSubAction('read');
151 
152  // return object list only
153  $object = $subResponse->getValue('object');
154  $response->clearValues();
155  if ($object != null) {
156  $response->setValue($object->getOID()->__toString(), $object);
157  }
158  else {
159  $response->setStatus(Response::STATUS_404);
160  }
161 
162  // set the response headers
163  //$this->setLocationHeaderFromOid($request->getValue('oid'));
164  }
165  else {
166  // read all objects of the given type
167  // delegate further processing to ListController
168  $offset = 0;
169 
170  // parse headers
171 
172  // range
173  if ($request->hasHeader('Range')) {
174  if (preg_match('/^items=([\-]?[0-9]+)-([\-]?[0-9]+)$/', $request->getHeader('Range'), $matches)) {
175  $offset = intval($matches[1]);
176  $limit = intval($matches[2])-$offset+1;
177  $request->setValue('offset', $offset);
178  $request->setValue('limit', $limit);
179  }
180  }
181 
182  // parse get paramters
183  foreach ($request->getValues() as $key => $value) {
184  // sort definition
185  if (preg_match('/^sort\(([^\)]+)\)$|sortBy=([.]+)$/', $key, $matches)) {
186  $sortDefs = preg_split('/,/', $matches[1]);
187  // ListController allows only one sortfield
188  $sortDef = $sortDefs[0];
189  $sortFieldName = substr($sortDef, 1);
190  $sortDirection = preg_match('/^-/', $sortDef) ? 'desc' : 'asc';
191  $request->setValue('sortFieldName', $sortFieldName);
192  $request->setValue('sortDirection', $sortDirection);
193  break;
194  }
195  // limit
196  if (preg_match('/^limit\(([^\)]+)\)$/', $key, $matches)) {
197  $rangeDefs = preg_split('/,/', $matches[1]);
198  $limit = intval(array_pop($rangeDefs));
199  $offset = sizeof($rangeDefs) > 0 ? intval($rangeDefs[0]) : 0;
200  $request->setValue('offset', $offset);
201  $request->setValue('limit', $limit);
202  break;
203  }
204  }
205 
206  // create query from optional GET values
207  if ($request->hasValue('className')) {
208  $operatorMap = array('eq' => '=', 'ne' => '!=', 'lt' => '<', 'lte' => '<=',
209  'gt' => '>', 'gte' => '>=', 'in' => 'in', 'match' => 'regexp');
210  $mapper = $persistenceFacade->getMapper($request->getValue('className'));
211  $type = $mapper->getType();
212  $simpleType = $persistenceFacade->getSimpleType($type);
213  $objectQuery = new ObjectQuery($type);
214  foreach ($request->getValues() as $name => $value) {
215  if (strpos($name, '.') > 0) {
216  // check name for type.attribute
217  list($typeInName, $attributeInName) = preg_split('/\.+(?=[^\.]+$)/', $name);
218  if (($typeInName == $type || $typeInName == $simpleType) &&
219  $mapper->hasAttribute($attributeInName)) {
220  $queryTemplate = $objectQuery->getObjectTemplate($type);
221  // handle null values correctly
222  $value = strtolower($value) == 'null' ? null : $value;
223  // extract optional operator from value e.g. lt=2015-01-01
224  $parts = explode('=', $value);
225  $op = $parts[0];
226  if (sizeof($parts) > 0 && isset($operatorMap[$op])) {
227  $operator = $operatorMap[$op];
228  $value = $parts[1];
229  }
230  else {
231  $operator = '=';
232  }
233  $queryTemplate->setValue($attributeInName, Criteria::asValue($operator, $value));
234  }
235  }
236  }
237  $query = $objectQuery->getQueryCondition();
238  if (strlen($query) > 0) {
239  $request->setValue('query', $query);
240  }
241  }
242 
243  // rewrite query if querying for a relation
244  if ($request->hasValue("relation") && $request->hasValue('sourceOid')) {
245  $relationName = $request->getValue("relation");
246 
247  // set the query
248  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
249  $sourceNode = $persistenceFacade->load($sourceOid);
250  if ($sourceNode) {
251  $query = NodeUtil::getRelationQueryCondition($sourceNode, $relationName);
252  $request->setValue('query', $query);
253 
254  // set the class name
255  $mapper = $sourceNode->getMapper();
256  $relation = $mapper->getRelation($relationName);
257  $otherType = $relation->getOtherType();
258  $request->setValue('className', $otherType);
259 
260  // set order
261  $otherMapper = $persistenceFacade->getMapper($otherType);
262  $sortkeyDef = $otherMapper->getSortkey($relation->getThisRole());
263  if ($sortkeyDef != null) {
264  $request->setValue('sortFieldName', $sortkeyDef['sortFieldName']);
265  $request->setValue('sortDirection', $sortkeyDef['sortDirection']);
266  }
267  }
268  }
269 
270  // execute action
271  $subResponse = $this->executeSubAction('list');
272 
273  // return object list only
274  $objects = $subResponse->getValue('list');
275  $response->clearValues();
276  $response->setValues($objects);
277 
278  // set response range header
279  $size = sizeof($objects);
280  $limit = $size == 0 ? $offset : $offset+$size-1;
281  $total = $subResponse->getValue('totalCount');
282  $response->setHeader('Content-Range', 'items '.$offset.'-'.$limit.'/'.$total);
283  }
284  }
285 
286  /**
287  * @see Controller::assignResponseDefaults()
288  */
289  protected function assignResponseDefaults() {
290  if (sizeof($this->getResponse()->getErrors()) > 0) {
291  parent::assignResponseDefaults();
292  }
293  // don't add anything in case of success
294  }
295 
296  /**
297  * Handle a POST request (create an object of a given type)
298  *
299  * | Parameter | Description
300  * |------------------|-------------------------
301  * | _in_ `language` | The language of the object
302  * | _in_ `className` | Type of object to create
303  * | _in_ `relation` | Relation name if the object should be created/added in relation to another object (determines the type of the created/added object)
304  * | _in_ `sourceId` | Id of the object to which the created objects are added (determines the object id together with _className_)
305  * | _out_ | Created object data
306  *
307  * The object data is contained in POST content. If an existing object
308  * should be added, an `oid` parameter in the object data is sufficient.
309  */
310  protected function handlePost() {
311  $request = $this->getRequest();
312  if ($request->hasValue('relation') && $request->hasValue('sourceOid')) {
313  // create new object
314  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
315  $relatedType = $this->getRelatedType($sourceOid, $request->getValue('relation'));
316  $request->setValue('className', $relatedType);
317  $subResponseCreate = $this->executeSubAction('create');
318 
319  $targetOid = $subResponseCreate->getValue('oid');
320  $targetOidStr = $targetOid->__toString();
321 
322  // add new object to relation
323  $request->setValue('targetOid', $targetOidStr);
324  $request->setValue('role', $request->getValue('relation'));
325  $subResponse = $this->executeSubAction('associate');
326 
327  // add related object to subresponse similar to default update action
328  $persistenceFacade = $this->getPersistenceFacade();
329  $targetObj = $persistenceFacade->load($targetOid);
330  $subResponse->setValue('oid', $targetOid);
331  $subResponse->setValue($targetOidStr, $targetObj);
332  }
333  else {
334  $subResponse = $this->executeSubAction('create');
335  }
336  $response = $this->getResponse();
337 
338  // in case of success, return object only
339  $oidStr = $subResponse->hasValue('oid') ? $subResponse->getValue('oid')->__toString() : '';
340  if (!$subResponse->hasErrors()) {
341  $response->clearValues();
342  if ($subResponse->hasValue($oidStr)) {
343  $object = $subResponse->getValue($oidStr);
344  $response->setValue($oidStr, $object);
345  }
346  $response->setStatus(Response::STATUS_201);
347  $this->setLocationHeaderFromOid($response->getValue('oid'));
348  }
349  else {
350  // in case of error, return default response
351  $response->setValues($subResponse->getValues());
352  $response->setStatus(Response::STATUS_400);
353  }
354  }
355 
356  /**
357  * Handle a PUT request (update an object of a given type)
358  *
359  * | Parameter | Description
360  * |------------------|-------------------------
361  * | _in_ `language` | The language of the object
362  * | _in_ `className` | Type of object to update
363  * | _in_ `id` | Id of object to update
364  * | _in_ `relation` | Relation name if an existing object should be added to a relation (determines the type of the added object)
365  * | _in_ `sourceId` | Id of the object to which the added object is related (determines the object id together with _className_)
366  * | _in_ `targetId` | Id of the object to be added to the relation (determines the object id together with _relation_)
367  * | _out_ | Updated object data
368  *
369  * The object data is contained in POST content.
370  */
371  protected function handlePut() {
372  $request = $this->getRequest();
373 
374  // check position header for reordering
375  $orderReferenceOid = null;
376  if ($request->hasHeader('Position')) {
377  $position = $request->getHeader('Position');
378  if ($position == 'last') {
379  $orderReferenceOid = 'ORDER_BOTTOM';
380  }
381  else {
382  list($ignore, $orderReferenceIdStr) = preg_split('/ /', $position);
383  if ($request->hasValue('relation') && $request->hasValue('sourceOid')) {
384  // sort in relation
385  $sourceOid = ObjectId::parse($request->getValue('sourceOid'));
386  $relatedType = $this->getRelatedType($sourceOid, $request->getValue('relation'));
387  $orderReferenceOid = new ObjectId($relatedType, $orderReferenceIdStr);
388  }
389  else {
390  // sort in root
391  $orderReferenceOid = new ObjectId($request->getValue('className'), $orderReferenceIdStr);
392  }
393  }
394  }
395 
396  if ($request->hasValue('relation') && $request->hasValue('sourceOid') &&
397  $request->hasValue('targetOid')) {
398  if ($orderReferenceOid != null) {
399  // change order in a relation
400  $request->setValue('containerOid', $request->getValue('sourceOid'));
401  $request->setValue('insertOid', $request->getValue('targetOid'));
402  $request->setValue('referenceOid', $orderReferenceOid);
403  $request->setValue('role', $request->getValue('relation'));
404  $subResponse = $this->executeSubAction('insertBefore');
405  }
406  else {
407  // add existing object to relation
408  $request->setValue('role', $request->getValue('relation'));
409  $subResponse = $this->executeSubAction('associate');
410  if ($subResponse->getStatus() == Response::STATUS_200) {
411  // and update object
412  $subResponse = $this->executeSubAction('update');
413  }
414  }
415 
416  // add related object to subresponse similar to default update action
417  if ($subResponse->getStatus() == Response::STATUS_200) {
418  $targetOidStr = $request->getValue('targetOid');
419  $targetOid = ObjectId::parse($targetOidStr);
420  $persistenceFacade = $this->getPersistenceFacade();
421  $targetObj = $persistenceFacade->load($targetOid);
422  $subResponse->setValue('oid', $targetOid);
423  $subResponse->setValue($targetOidStr, $targetObj);
424  }
425  }
426  else {
427  if ($orderReferenceOid != null) {
428  // change order in a relation
429  $request->setValue('insertOid', $this->getFirstRequestOid());
430  $request->setValue('referenceOid', $orderReferenceOid);
431  $subResponse = $this->executeSubAction('moveBefore');
432 
433  // add sorted object to subresponse similar to default update action
434  if ($subResponse->getStatus() == Response::STATUS_200) {
435  $targetOidStr = $this->getFirstRequestOid();
436  $targetOid = ObjectId::parse($targetOidStr);
437  $persistenceFacade = $this->getPersistenceFacade();
438  $targetObj = $persistenceFacade->load($targetOid);
439  $subResponse->setValue('oid', $targetOid);
440  $subResponse->setValue($targetOidStr, $targetObj);
441  }
442  }
443  else {
444  // update object
445  $subResponse = $this->executeSubAction('update');
446  }
447  }
448  $response = $this->getResponse();
449 
450  // in case of success, return object only
451  $oidStr = $this->getFirstRequestOid();
452  if (!$subResponse->hasErrors()) {
453  $response->clearValues();
454  if ($subResponse->hasValue($oidStr)) {
455  $object = $subResponse->getValue($oidStr);
456  $response->setValue($oidStr, $object);
457  }
458  $response->setStatus(Response::STATUS_202);
459  $this->setLocationHeaderFromOid($response->getValue('oid'));
460  }
461  else {
462  // in case of error, return default response
463  $response->setValues($subResponse->getValues());
464  $response->setStatus(Response::STATUS_400);
465  }
466  }
467 
468  /**
469  * Handle a DELETE request (delete an object of a given type)
470  *
471  * | Parameter | Description
472  * |------------------|-------------------------
473  * | _in_ `language` | The language of the object
474  * | _in_ `className` | Type of object to delete
475  * | _in_ `id` | Id of object to delete
476  * | _in_ `relation` | Relation name if the object should be deleted from a relation to another object (determines the type of the deleted object)
477  * | _in_ `sourceId` | Id of the object to which the deleted object is related (determines the object id together with _className_)
478  * | _in_ `targetId` | Id of the object to be deleted from the relation (determines the object id together with _relation_)
479  */
480  protected function handleDelete() {
481  $request = $this->getRequest();
482  if ($request->hasValue('relation') && $request->hasValue('sourceOid') &&
483  $request->hasValue('targetOid')) {
484  // remove existing object from relation
485  $request->setValue('role', $request->getValue('relation'));
486  $subResponse = $this->executeSubAction('disassociate');
487  }
488  else {
489  // delete object
490  $subResponse = $this->executeSubAction('delete');
491  }
492  $response = $this->getResponse();
493  $response->setValues($subResponse->getValues());
494 
495  if (!$subResponse->hasErrors()) {
496  // set the response headers
497  $response->setStatus(Response::STATUS_204);
498  }
499  else {
500  // in case of error, return default response
501  $response->setStatus(Response::STATUS_400);
502  }
503  }
504 
505  /**
506  * Set the location response header according to the given object id
507  * @param $oid The serialized object id
508  */
509  protected function setLocationHeaderFromOid($oid) {
510  $oid = ObjectId::parse($oid);
511  if ($oid) {
512  $response = $this->getResponse();
513  $response->setHeader('Location', $oid->__toString());
514  }
515  }
516 
517  /**
518  * Get the first oid from the request
519  * @return String
520  */
521  protected function getFirstRequestOid() {
522  $request = $this->getRequest();
523  foreach ($request->getValues() as $key => $value) {
524  if (ObjectId::isValid($key)) {
525  return $key;
526  }
527  }
528  return '';
529  }
530 
531  /**
532  * Get the type that is used in the given role related to the
533  * given source object.
534  * @param $sourceOid ObjectId of the source object
535  * @param $role The role name
536  * @return String
537  */
538  protected function getRelatedType(ObjectId $sourceOid, $role) {
539  $persistenceFacade = $this->getPersistenceFacade();
540  $sourceMapper = $persistenceFacade->getMapper($sourceOid->getType());
541  $relation = $sourceMapper->getRelation($role);
542  return $relation->getOtherType();
543  }
544 }
545 ?>
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
getRequest()
Get the Request instance.
Definition: Controller.php:190
RESTController handles REST requests.
setLocationHeaderFromOid($oid)
Set the location response header according to the given object id.
static getRelationQueryCondition($node, $otherRole)
Get the query condition used to select all related Nodes of a given role.
Definition: NodeUtil.php:117
initialize(Request $request, Response $response)
Controller is the base class of all controllers.
Definition: Controller.php:48
getFirstRequestOid()
Get the first oid from the request.
getRelatedType(ObjectId $sourceOid, $role)
Get the type that is used in the given role related to the given source object.
handleDelete()
Handle a DELETE request (delete an object of a given type)
ObjectQuery implements a template based object query.
static asValue($operator, $value)
Factory method for constructing a Critera that may be used as value on a PersistentObject's attribute...
Definition: Criteria.php:58
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
handlePut()
Handle a PUT request (update an object of a given type)
Request holds the request values that are used as input to Controller instances.
Definition: Request.php:20
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:144
hasValue($name)
Check for existance of a value.
executeSubAction($action)
Delegate the current request to another action.
Definition: Controller.php:172
Application controllers.
Definition: namespaces.php:3
setValue($name, $value)
Set a value.
static isValid($oid)
Check if a serialized ObjectId has a valid syntax, the type is known and if the number of primary key...
Definition: ObjectId.php:132
handlePost()
Handle a POST request (create an object of a given type)
static get($code, $data=null)
Factory method for retrieving a predefind error instance.
handleGet()
Handle a GET request (read object(s) of a given type)
getType()
Get the type (including namespace)
Definition: ObjectId.php:106
getResponse()
Get the Response instance.
Definition: Controller.php:198
getValue($name, $default=null, $filter=null, $options=null)
Get a value.
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:222