3 * Licensed to the Apache Software Foundation (ASF) under one
4 * or more contributor license agreements. See the NOTICE file
5 * distributed with this work for additional information
6 * regarding copyright ownership. The ASF licenses this file
7 * to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance
9 * with the License. You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing,
14 * software distributed under the License is distributed on an
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 * KIND, either express or implied. See the License for the
17 * specific language governing permissions and limitations
22 * Implementation of supported services backed by a JSON DB
24 class JsonDbOpensocialService
implements ActivityService
, PersonService
, AppDataService
, MessagesService
{
32 * db["activities"] -> Array<Person>
34 private static $PEOPLE_TABLE = "people";
37 * db["people"] -> Map<Person.Id, Array<Activity>>
39 private static $ACTIVITIES_TABLE = "activities";
42 * db["data"] -> Map<Person.Id, Map<String, String>>
44 private static $DATA_TABLE = "data";
47 * db["friendLinks"] -> Map<Person.Id, Array<Person.Id>>
49 private static $FRIEND_LINK_TABLE = "friendLinks";
52 * db["userApplications"] -> Map<Person.Id, Array<Application Ids>>
54 private static $USER_APPLICATIONS_TABLE = "userApplications";
56 private $allPeople = null;
58 private $allData = null;
60 private $allActivities = null;
62 private $jsonDbFileName = 'ShindigDb.json';
64 public function getDb() {
66 $fileName = sys_get_temp_dir() . '/' . $this->jsonDbFileName
;
67 if (file_exists($fileName)) {
68 if (! is_readable($fileName)) {
69 throw new SocialSpiException("Could not read temp json db file: $fileName, check permissions", ResponseError
::$INTERNAL_ERROR);
71 $cachedDb = file_get_contents($fileName);
72 $jsonDecoded = json_decode($cachedDb, true);
73 if ($jsonDecoded == $cachedDb ||
$jsonDecoded == null) {
74 throw new SocialSpiException("Failed to decode the json db", ResponseError
::$INTERNAL_ERROR);
78 $jsonDb = Config
::get('jsondb_path');
79 if (! file_exists($jsonDb) ||
! is_readable($jsonDb)) {
80 throw new SocialSpiException("Could not read json db file: $jsonDb, check if the file exists & has proper permissions", ResponseError
::$INTERNAL_ERROR);
82 $dbConfig = @file_get_contents
($jsonDb);
83 $contents = preg_replace('/(?<!http:|https:)\/\/.*$/m', '', preg_replace('@/\\*(?:.|[\\n\\r])*?\\*/@', '', $dbConfig));
84 $jsonDecoded = json_decode($contents, true);
85 if ($jsonDecoded == $contents ||
$jsonDecoded == null) {
86 throw new SocialSpiException("Failed to decode the json db", ResponseError
::$INTERNAL_ERROR);
88 $this->saveDb($jsonDecoded);
91 } catch (Exception
$e) {
92 throw new SocialSpiException("An error occured while reading/writing the json db: " . $e->getMessage(), ResponseError
::$INTERNAL_ERROR);
96 private function saveDb($db) {
97 if (! @file_put_contents
(sys_get_temp_dir() . '/' . $this->jsonDbFileName
, json_encode($db))) {
98 throw new Exception("Could not save json db: " . sys_get_temp_dir() . '/' . $this->jsonDbFileName
);
102 private function getAllPeople() {
103 $db = $this->getDb();
104 $peopleTable = $db[self
::$PEOPLE_TABLE];
105 foreach ($peopleTable as $people) {
106 $this->allPeople
[$people['id']] = $people;
108 $db[self
::$PEOPLE_TABLE] = $this->allPeople
;
109 return $this->allPeople
;
112 private function getAllData() {
113 $db = $this->getDb();
114 $dataTable = $db[self
::$DATA_TABLE];
115 foreach ($dataTable as $key => $value) {
116 $this->allData
[$key] = $value;
118 $db[self
::$DATA_TABLE] = $this->allData
;
119 return $this->allData
;
122 private function getAllActivities() {
123 $db = $this->getDb();
124 $activitiesTable = $db[self
::$ACTIVITIES_TABLE];
125 foreach ($activitiesTable as $key => $value) {
126 $this->allActivities
[$key] = $value;
128 $db[self
::$ACTIVITIES_TABLE] = $this->allActivities
;
129 return $this->allActivities
;
132 private function getPeopleWithApp($appId) {
133 $peopleWithApp = array();
134 $db = $this->getDb();
135 $userApplicationsTable = $db[self
::$USER_APPLICATIONS_TABLE];
136 foreach ($userApplicationsTable as $key => $value) {
137 if (in_array($appId, $userApplicationsTable[$key])) {
138 $peopleWithApp[] = $key;
141 return $peopleWithApp;
144 public function getPerson($userId, $groupId, $fields, SecurityToken
$token) {
145 if (! is_object($groupId)) {
146 throw new SocialSpiException("Not Implemented", ResponseError
::$NOT_IMPLEMENTED);
148 $person = $this->getPeople($userId, $groupId, new CollectionOptions(), $fields, $token);
149 if (is_array($person->getEntry())) {
150 $person = $person->getEntry();
151 if (is_array($person) && count($person) == 1) {
152 return array_pop($person);
155 throw new SocialSpiException("Person not found", ResponseError
::$BAD_REQUEST);
158 public function getPeople($userId, $groupId, CollectionOptions
$options, $fields, SecurityToken
$token) {
160 $sortOrder = $options->getSortOrder();
161 $filter = $options->getFilterBy();
162 $first = $options->getStartIndex();
163 $max = $options->getCount();
164 $networkDistance = $options->getNetworkDistance();
165 $ids = $this->getIdSet($userId, $groupId, $token);
166 $allPeople = $this->getAllPeople();
167 if (! $token->isAnonymous() && $filter == "hasApp") {
168 $appId = $token->getAppId();
169 $peopleWithApp = $this->getPeopleWithApp($appId);
172 foreach ($ids as $id) {
173 if ($filter == "hasApp" && ! in_array($id, $peopleWithApp)) {
177 if (is_array($allPeople) && isset($allPeople[$id])) {
178 $person = $allPeople[$id];
179 if (! $token->isAnonymous() && $id == $token->getViewerId()) {
180 $person['isViewer'] = true;
182 if (! $token->isAnonymous() && $id == $token->getOwnerId()) {
183 $person['isOwner'] = true;
185 if ($fields[0] != '@all') {
186 $newPerson = array();
187 $newPerson['isOwner'] = isset($person['isOwner']) ?
$person['isOwner'] : false;
188 $newPerson['isViewer'] = isset($person['isViewer']) ?
$person['isViewer'] : false;
189 $newPerson['name'] = $person['name'];
190 $newPerson['displayName'] = $person['displayName'];
191 foreach ($fields as $field => $present) {
192 $present = strtolower($present);
193 if (isset($person[$present]) && ! isset($newPerson[$present])) {
194 $newPerson[$present] = $person[$present];
197 $person = $newPerson;
199 $people[$id] = $person;
202 if ($sortOrder == 'name') {
203 usort($people, array($this, 'comparator'));
207 $people = $this->filterResults($people, $options);
208 } catch(Exception
$e) {
209 $people['filtered'] = 'false';
212 //TODO: The samplecontainer doesn't support any filters yet. We should fix this.
213 $totalSize = count($people);
214 $collection = new RestfulCollection($people, $options->getStartIndex(), $totalSize);
215 $collection->setItemsPerPage($options->getCount());
219 private function filterResults($peopleById, $options) {
220 if (! $options->getFilterBy()) {
221 return $peopleById; // no filtering specified
223 $filterBy = $options->getFilterBy();
224 $op = $options->getFilterOperation();
226 $op = CollectionOptions
::FILTER_OP_EQUALS
; // use this container-specific default
228 $value = $options->getFilterValue();
229 $filteredResults = array();
230 $numFilteredResults = 0;
231 foreach ($peopleById as $id => $person) {
232 if ($this->passesFilter($person, $filterBy, $op, $value)) {
233 $filteredResults[$id] = $person;
234 $numFilteredResults ++
;
237 return $filteredResults;
240 private function passesFilter($person, $filterBy, $op, $value) {
241 $fieldValue = $person[$filterBy];
242 if (! $fieldValue ||
(is_array($fieldValue) && ! count($fieldValue))) {
243 return false; // person is missing the field being filtered for
245 if ($op == CollectionOptions
::FILTER_OP_PRESENT
) {
246 return true; // person has a non-empty value for the requested field
249 return false; // can't do an equals/startswith/contains filter on an empty filter value
251 // grab string value for comparison
252 if (is_array($fieldValue)) {
253 // plural fields match if any instance of that field matches
254 foreach ($fieldValue as $field) {
255 if ($this->passesStringFilter($field, $op, $value)) {
260 return $this->passesStringFilter($fieldValue, $op, $value);
266 public function getPersonData($userId, GroupId
$groupId, $appId, $fields, SecurityToken
$token) {
267 if (! isset($fields[0])) {
270 $db = $this->getDb();
271 $allData = $this->getAllData();
272 $friendsTable = $db[self
::$FRIEND_LINK_TABLE];
274 $ids = $this->getIdSet($userId, $groupId, $token);
275 foreach ($ids as $id) {
276 if (isset($allData[$id])) {
277 $allPersonData = $allData[$id];
278 $personData = array();
279 foreach (array_keys($allPersonData) as $key) {
280 if (in_array($key, $fields) ||
in_array("@all", $fields)) {
281 $personData[$key] = $allPersonData[$key];
284 $data[$id] = $personData;
287 return new DataCollection($data);
290 public function updatePersonData(UserId
$userId, GroupId
$groupId, $appId, $fields, $values, SecurityToken
$token) {
291 $db = $this->getDb();
292 foreach ($fields as $key => $present) {
293 if (! $this->isValidKey($present)) {
294 throw new SocialSpiException("The person app data key had invalid characters", ResponseError
::$BAD_REQUEST);
297 $allData = $this->getAllData();
298 $person = $allData[$userId->getUserId($token)];
299 switch ($groupId->getType()) {
301 foreach ($fields as $key => $present) {
302 $value = isset($values[$present]) ?
@$values[$present] : null;
303 $person[$present] = $value;
307 throw new SocialSpiException("We don't support updating data in batches yet", ResponseError
::$NOT_IMPLEMENTED);
310 $allData[$userId->getUserId($token)] = $person;
311 $db[self
::$DATA_TABLE] = $allData;
316 public function deletePersonData($userId, GroupId
$groupId, $appId, $fields, SecurityToken
$token) {
317 $db = $this->getDb();
318 $allData = $this->getAllData();
319 if ($fields == null ||
$fields[0] == '*') {
320 $allData[$userId->getUserId($token)] = null;
321 $db[self
::$DATA_TABLE] = $allData;
325 foreach ($fields as $key => $present) {
326 if (! $this->isValidKey($key)) {
327 throw new SocialSpiException("The person app data key had invalid characters", ResponseError
::$BAD_REQUEST);
330 switch ($groupId->getType()) {
332 foreach ($fields as $key => $present) {
333 $value = isset($values[$key]) ?
null : @$values[$key];
334 $person[$key] = $value;
336 $allData[$userId->getUserId($token)] = $person;
337 $db[self
::$DATA_TABLE] = $allData;
341 throw new SocialSpiException("We don't support updating data in batches yet", ResponseError
::$NOT_IMPLEMENTED);
347 public function getActivity($userId, $groupId, $appdId, $fields, $activityId, SecurityToken
$token) {
348 $activities = $this->getActivities($userId, $groupId, $appdId, null, null, null, null, $fields, array($activityId), $token);
349 if ($activities instanceof RestfulCollection
) {
350 $activities = $activities->getEntry();
351 foreach ($activities as $activity) {
352 if ($activity->getId() == $activityId) {
357 throw new SocialSpiException("Activity not found", ResponseError
::$NOT_FOUND);
360 public function getActivities($userIds, $groupId, $appId, $sortBy, $filterBy, $filterOp, $filterValue, $startIndex, $count, $fields, $activityIds, $token) {
361 $db = $this->getDb();
362 $friendsTable = $db[self
::$FRIEND_LINK_TABLE];
364 $ids = $this->getIdSet($userIds, $groupId, $token);
365 $allActivities = $this->getAllActivities();
366 $activities = array();
367 foreach ($ids as $id) {
368 if (isset($allActivities[$id])) {
369 $personsActivities = $allActivities[$id];
370 $activities = array_merge($activities, $personsActivities);
372 $newPersonsActivities = array();
373 foreach ($personsActivities as $activity) {
374 $newActivity = array();
375 foreach ($fields as $field => $present) {
376 $newActivity[$present] = $activity[$present];
378 $newPersonsActivities[] = $newActivity;
380 $personsActivities = $newPersonsActivities;
381 $activities = $personsActivities;
383 if ($filterBy && $filterValue) {
384 $newActivities = array();
385 foreach ($activities as $activity) {
386 if (array_key_exists($filterBy, $activity)) {
387 if ($this->passesStringFilter($activity[$filterBy], $filterOp, $filterValue)) {
388 $newActivities[] = $activity;
391 throw new SocialSpiException("Invalid filterby parameter", ResponseError
::$NOT_FOUND);
394 $activities = $newActivities;
398 $totalResults = count($activities);
399 if (! $totalResults) {
400 throw new SocialSpiException("Activity not found", ResponseError
::$NOT_FOUND);
402 $activities = array_slice($activities, $startIndex, $count);
403 $ret = new RestfulCollection($activities, $startIndex, $totalResults);
404 $ret->setItemsPerPage($count);
410 * to check the activity against filter
413 private function passesStringFilter($fieldValue, $filterOp, $filterValue) {
415 case CollectionOptions
::FILTER_OP_EQUALS
:
416 return $fieldValue == $filterValue;
417 case CollectionOptions
::FILTER_OP_CONTAINS
:
418 return strpos($fieldValue, $filterValue) !== false;
419 case CollectionOptions
::FILTER_OP_STARTSWITH
:
420 return strpos($fieldValue, $filterValue) === 0;
422 throw new Exception('unrecognized filterOp');
426 public function createActivity($userId, $groupId, $appId, $fields, $activity, SecurityToken
$token) {
427 $db = $this->getDb();
428 $activitiesTable = $this->getAllActivities();
429 $activity['appId'] = $token->getAppId();
431 array_push($activitiesTable[$userId->getUserId($token)], $activity);
432 $db[self
::$ACTIVITIES_TABLE] = $activitiesTable;
434 // Should this return something to show success?
435 } catch (Exception
$e) {
436 throw new SocialSpiException("Activity can't be created: " . $e->getMessage(), ResponseError
::$INTERNAL_ERROR);
440 public function deleteActivities($userId, $groupId, $appId, $activityIds, SecurityToken
$token) {
441 throw new SocialSpiException("Not implemented", ResponseError
::$NOT_IMPLEMENTED);
444 public function createMessage($userId, $appId, $message, $optionalMessageId, SecurityToken
$token) {
445 throw new SocialSpiException("Not implemented", ResponseError
::$NOT_IMPLEMENTED);
449 * Determines whether the input is a valid key.
451 * @param key the key to validate.
452 * @return true if the key is a valid appdata key, false otherwise.
454 public static function isValidKey($key) {
458 for ($i = 0; $i < strlen($key); ++
$i) {
459 $c = substr($key, $i, 1);
460 if (($c >= 'a' && $c <= 'z') ||
($c >= 'A' && $c <= 'Z') ||
($c >= '0' && $c <= '9') ||
($c == '-') ||
($c == '_') ||
($c == '.')) {
468 private function comparator($person, $person1) {
469 $name = $person['name']['unstructured'];
470 $name1 = $person1['name']['unstructured'];
471 if ($name == $name1) {
474 return ($name < $name1) ?
- 1 : 1;
478 * Get the set of user id's from a user or collection of users, and group
479 * Code taken from http://code.google.com/p/partuza/source/browse/trunk/Shindig/PartuzaService.php
481 private function getIdSet($user, GroupId
$group, SecurityToken
$token) {
483 $db = $this->getDb();
484 $friendsTable = $db[self
::$FRIEND_LINK_TABLE];
485 if ($user instanceof UserId
) {
486 $userId = $user->getUserId($token);
487 if ($group == null) {
488 return array($userId);
490 switch ($group->getType()) {
496 if (is_array($friendsTable) && count($friendsTable) && isset($friendsTable[$userId])) {
497 $ids = $friendsTable[$userId];
501 return new ResponseItem(NOT_IMPLEMENTED
, "We don't support fetching data in batches yet", null);
504 } elseif (is_array($user)) {
506 foreach ($user as $id) {
507 $ids = array_merge($ids, $this->getIdSet($id, $group, $token));