PageRenderTime 26ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 1ms

/src/RestControllers/RestControllerHelper.php

https://github.com/openemr/openemr
PHP | 339 lines | 234 code | 34 blank | 71 comment | 43 complexity | 04cbad4edf79dc3ef958f245adfe5309 MD5 | raw file
Possible License(s): GPL-3.0, Apache-2.0, LGPL-2.1, AGPL-1.0
  1. <?php
  2. /**
  3. * RestControllerHelper
  4. *
  5. * @package OpenEMR
  6. * @link http://www.open-emr.org
  7. * @author Matthew Vita <matthewvita48@gmail.com>
  8. * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
  9. * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
  10. */
  11. namespace OpenEMR\RestControllers;
  12. use OpenEMR\Common\Logging\SystemLogger;
  13. use OpenEMR\Services\FHIR\IResourceSearchableService;
  14. use OpenEMR\Services\Search\FhirSearchParameterDefinition;
  15. use OpenEMR\Services\Search\SearchFieldType;
  16. use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRPatient;
  17. use OpenEMR\FHIR\R4\FHIRElement\FHIRCanonical;
  18. use OpenEMR\FHIR\R4\FHIRElement\FHIRCode;
  19. use OpenEMR\FHIR\R4\FHIRElement\FHIRExtension;
  20. use OpenEMR\FHIR\R4\FHIRElement\FHIRRestfulCapabilityMode;
  21. use OpenEMR\FHIR\R4\FHIRElement\FHIRTypeRestfulInteraction;
  22. use OpenEMR\FHIR\R4\FHIRResource;
  23. use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementInteraction;
  24. use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementOperation;
  25. use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementResource;
  26. use OpenEMR\FHIR\R4\FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementRest;
  27. use OpenEMR\Services\FHIR\IResourceUSCIGProfileService;
  28. use OpenEMR\Validators\ProcessingResult;
  29. class RestControllerHelper
  30. {
  31. /**
  32. * The resource endpoint names we want to skip over.
  33. */
  34. const IGNORE_ENDPOINT_RESOURCES = ['.well-known', 'metadata'];
  35. /**
  36. * The default FHIR services class namespace
  37. * TODO: should we build a fhir service locator class? There are two places this is now used, in this class and
  38. * in the FhirProvenanceService...
  39. */
  40. const FHIR_SERVICES_NAMESPACE = "OpenEMR\\Services\\FHIR\\Fhir";
  41. // @see https://www.hl7.org/fhir/search.html#table
  42. const FHIR_SEARCH_CONTROL_PARAM_REV_INCLUDE_PROVENANCE = "Provenance:target";
  43. /**
  44. * Configures the HTTP status code and payload returned within a response.
  45. *
  46. * @param $serviceResult
  47. * @param $customRespPayload
  48. * @param $idealStatusCode
  49. * @return null
  50. */
  51. public static function responseHandler($serviceResult, $customRespPayload, $idealStatusCode)
  52. {
  53. if ($serviceResult) {
  54. http_response_code($idealStatusCode);
  55. if ($customRespPayload) {
  56. return $customRespPayload;
  57. }
  58. return $serviceResult;
  59. }
  60. // if no result is present return a 404 with a null response
  61. http_response_code(404);
  62. return null;
  63. }
  64. public static function validationHandler($validationResult)
  65. {
  66. if (property_exists($validationResult, 'isValid') && !$validationResult->isValid()) {
  67. http_response_code(400);
  68. $validationMessages = null;
  69. if (property_exists($validationResult, 'getValidationMessages')) {
  70. $validationMessages = $validationResult->getValidationMessages();
  71. } else {
  72. $validationMessages = $validationResult->getMessages();
  73. }
  74. return $validationMessages;
  75. }
  76. return null;
  77. }
  78. /**
  79. * Parses a service processing result for standard Apis to determine the appropriate HTTP status code and response format
  80. * for a request.
  81. *
  82. * The response body has a uniform structure with the following top level keys:
  83. * - validationErrors
  84. * - internalErrors
  85. * - data
  86. *
  87. * The response data key conveys the data payload for a response. The payload is either a "top level" array for a
  88. * single result, or an array for multiple results.
  89. *
  90. * @param $processingResult - The service processing result.
  91. * @param $successStatusCode - The HTTP status code to return for a successful operation that completes without error.
  92. * @param $isMultipleResultResponse - Indicates if the response contains multiple results.
  93. * @return array[]
  94. */
  95. public static function handleProcessingResult($processingResult, $successStatusCode, $isMultipleResultResponse = false): array
  96. {
  97. $httpResponseBody = [
  98. "validationErrors" => [],
  99. "internalErrors" => [],
  100. "data" => []
  101. ];
  102. if (!$processingResult->isValid()) {
  103. http_response_code(400);
  104. $httpResponseBody["validationErrors"] = $processingResult->getValidationMessages();
  105. (new SystemLogger())->debug("RestControllerHelper::handleProcessingResult() 400 error", ['validationErrors' => $processingResult->getValidationMessages()]);
  106. } elseif ($processingResult->hasInternalErrors()) {
  107. http_response_code(500);
  108. $httpResponseBody["internalErrors"] = $processingResult->getInternalErrors();
  109. (new SystemLogger())->debug("RestControllerHelper::handleProcessingResult() 500 error", ['internalErrors' => $processingResult->getValidationMessages()]);
  110. } else {
  111. http_response_code($successStatusCode);
  112. $dataResult = $processingResult->getData();
  113. $recordsCount = count($dataResult);
  114. (new SystemLogger())->debug("RestControllerHelper::handleFhirProcessingResult() Records found", ['count' => $recordsCount]);
  115. if (!$isMultipleResultResponse) {
  116. $dataResult = ($recordsCount === 0) ? [] : $dataResult[0];
  117. }
  118. $httpResponseBody["data"] = $dataResult;
  119. }
  120. return $httpResponseBody;
  121. }
  122. /**
  123. * Parses a service processing result for FHIR endpoints to determine the appropriate HTTP status code and response format
  124. * for a request.
  125. *
  126. * The response body has a normal Fhir Resource json:
  127. *
  128. * @param $processingResult - The service processing result.
  129. * @param $successStatusCode - The HTTP status code to return for a successful operation that completes without error.
  130. * @return array|mixed
  131. */
  132. public static function handleFhirProcessingResult(ProcessingResult $processingResult, $successStatusCode)
  133. {
  134. $httpResponseBody = [];
  135. if (!$processingResult->isValid()) {
  136. http_response_code(400);
  137. $httpResponseBody["validationErrors"] = $processingResult->getValidationMessages();
  138. (new SystemLogger())->debug("RestControllerHelper::handleFhirProcessingResult() 400 error", ['validationErrors' => $processingResult->getValidationMessages()]);
  139. } elseif (count($processingResult->getData()) <= 0) {
  140. http_response_code(404);
  141. (new SystemLogger())->debug("RestControllerHelper::handleFhirProcessingResult() 404 records not found");
  142. } elseif ($processingResult->hasInternalErrors()) {
  143. http_response_code(500);
  144. (new SystemLogger())->debug("RestControllerHelper::handleFhirProcessingResult() 500 error", ['internalErrors' => $processingResult->getValidationMessages()]);
  145. $httpResponseBody["internalErrors"] = $processingResult->getInternalErrors();
  146. } else {
  147. http_response_code($successStatusCode);
  148. $dataResult = $processingResult->getData();
  149. (new SystemLogger())->debug("RestControllerHelper::handleFhirProcessingResult() Records found", ['count' => count($dataResult)]);
  150. $httpResponseBody = $dataResult[0];
  151. }
  152. return $httpResponseBody;
  153. }
  154. public function setSearchParams($resource, FHIRCapabilityStatementResource $capResource, $service)
  155. {
  156. if (empty($service)) {
  157. return; // nothing to do here as the service isn't defined.
  158. }
  159. if (!$service instanceof IResourceSearchableService) {
  160. return; // nothing to do here as the source is not searchable.
  161. }
  162. if (empty($capResource->getSearchInclude())) {
  163. $capResource->addSearchInclude('*');
  164. }
  165. if ($service instanceof IResourceUSCIGProfileService && empty($capResource->getSearchRevInclude())) {
  166. $capResource->addSearchRevInclude(self::FHIR_SEARCH_CONTROL_PARAM_REV_INCLUDE_PROVENANCE);
  167. }
  168. $searchParams = $service->getSearchParams();
  169. $searchParams = is_array($searchParams) ? $searchParams : [];
  170. foreach ($searchParams as $fhirSearchField => $searchDefinition) {
  171. /**
  172. * @var FhirSearchParameterDefinition $searchDefinition
  173. */
  174. $paramExists = false;
  175. $type = $searchDefinition->getType();
  176. foreach ($capResource->getSearchParam() as $searchParam) {
  177. if (strcmp($searchParam->getName(), $fhirSearchField) == 0) {
  178. $paramExists = true;
  179. }
  180. }
  181. if (!$paramExists) {
  182. $param = new FHIRResource\FHIRCapabilityStatement\FHIRCapabilityStatementSearchParam();
  183. $param->setName($fhirSearchField);
  184. $param->setType($type);
  185. $capResource->addSearchParam($param);
  186. }
  187. }
  188. }
  189. /**
  190. * Retrieves the fully qualified service class name for a given FHIR resource. It will only return a class that
  191. * actually exists.
  192. * @param $resource The name of the FHIR resource that we attempt to find the service class for.
  193. * @param string $serviceClassNameSpace The namespace to find the class in. Defaults to self::FHIR_SERVICES_NAMESPACE
  194. * @return string|null Returns the fully qualified name if the class is found, otherwise it returns null.
  195. */
  196. public function getFullyQualifiedServiceClassForResource($resource, $serviceClassNameSpace = self::FHIR_SERVICES_NAMESPACE)
  197. {
  198. $serviceClass = $serviceClassNameSpace . $resource . "Service";
  199. if (class_exists($serviceClass)) {
  200. return $serviceClass;
  201. }
  202. return null;
  203. }
  204. public function addOperations($resource, $items, FHIRCapabilityStatementResource $capResource)
  205. {
  206. $operation = end($items);
  207. // we want to skip over anything that's not a resource $operation
  208. if ($operation == '$export') {
  209. if ($resource != '$export') {
  210. $operationName = strtolower($resource) . '-export';
  211. } else {
  212. $operationName = 'export';
  213. }
  214. // define export operation
  215. $resource = new FHIRPatient();
  216. $fhirOperation = new FHIRCapabilityStatementOperation();
  217. $fhirOperation->setName($operation);
  218. $fhirOperation->setDefinition(new FHIRCanonical('http://hl7.org/fhir/uv/bulkdata/OperationDefinition/' . $operationName));
  219. $capResource->addOperation($fhirOperation);
  220. }
  221. }
  222. public function addRequestMethods($items, FHIRCapabilityStatementResource $capResource)
  223. {
  224. $reqMethod = trim($items[0], " ");
  225. $numberItems = count($items);
  226. $code = "";
  227. // we want to skip over $export operations.
  228. if (end($items) === '$export') {
  229. return;
  230. }
  231. // now setup our interaction types
  232. if (strcmp($reqMethod, "GET") == 0) {
  233. if (!empty(preg_match('/:/', $items[$numberItems - 1]))) {
  234. $code = "read";
  235. } else {
  236. $code = "search-type";
  237. }
  238. } elseif (strcmp($reqMethod, "POST") == 0) {
  239. $code = "insert";
  240. } elseif (strcmp($reqMethod, "PUT") == 0) {
  241. $code = "update";
  242. }
  243. if (!empty($code)) {
  244. $interaction = new FHIRCapabilityStatementInteraction();
  245. $restfulInteraction = new FHIRTypeRestfulInteraction();
  246. $restfulInteraction->setValue($code);
  247. $interaction->setCode($restfulInteraction);
  248. $capResource->addInteraction($interaction);
  249. }
  250. }
  251. public function getCapabilityRESTObject($routes, $serviceClassNameSpace = self::FHIR_SERVICES_NAMESPACE, $structureDefinition = "http://hl7.org/fhir/StructureDefinition/"): FHIRCapabilityStatementRest
  252. {
  253. $restItem = new FHIRCapabilityStatementRest();
  254. $mode = new FHIRRestfulCapabilityMode();
  255. $mode->setValue('server');
  256. $restItem->setMode($mode);
  257. $resourcesHash = array();
  258. foreach ($routes as $key => $function) {
  259. $items = explode("/", $key);
  260. if ($serviceClassNameSpace == self::FHIR_SERVICES_NAMESPACE) {
  261. // FHIR routes always have the resource at $items[2]
  262. $resource = $items[2];
  263. } else {
  264. // API routes do not always have the resource at $items[2]
  265. if (count($items) < 5) {
  266. $resource = $items[2];
  267. } elseif (count($items) < 7) {
  268. $resource = $items[4];
  269. if (substr($resource, 0, 1) === ':') {
  270. // special behavior needed for the API portal route
  271. $resource = $items[3];
  272. }
  273. } else { // count($items) < 9
  274. $resource = $items[6];
  275. }
  276. }
  277. if (!in_array($resource, self::IGNORE_ENDPOINT_RESOURCES)) {
  278. $service = null;
  279. $serviceClass = $this->getFullyQualifiedServiceClassForResource($resource, $serviceClassNameSpace);
  280. if (!empty($serviceClass)) {
  281. $service = new $serviceClass();
  282. }
  283. if (!array_key_exists($resource, $resourcesHash)) {
  284. $capResource = new FHIRCapabilityStatementResource();
  285. $capResource->setType(new FHIRCode($resource));
  286. $capResource->setProfile(new FHIRCanonical($structureDefinition . $resource));
  287. if ($service instanceof IResourceUSCIGProfileService) {
  288. $profileUris = $service->getProfileURIs();
  289. foreach ($profileUris as $uri) {
  290. $capResource->addSupportedProfile(new FHIRCanonical($uri));
  291. }
  292. }
  293. $resourcesHash[$resource] = $capResource;
  294. }
  295. $this->setSearchParams($resource, $resourcesHash[$resource], $service);
  296. $this->addRequestMethods($items, $resourcesHash[$resource]);
  297. $this->addOperations($resource, $items, $resourcesHash[$resource]);
  298. }
  299. }
  300. foreach ($resourcesHash as $resource => $capResource) {
  301. $restItem->addResource($capResource);
  302. }
  303. return $restItem;
  304. }
  305. }