PageRenderTime 84ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/server/batch_controller.go

https://gitlab.com/achedeuzot/fhir
Go | 326 lines | 257 code | 38 blank | 31 comment | 82 complexity | 41adc6e71b9fd2395026a6cc648170d6 MD5 | raw file
  1. package server
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "reflect"
  7. "regexp"
  8. "sort"
  9. "strings"
  10. "gopkg.in/mgo.v2/bson"
  11. "github.com/gin-gonic/gin"
  12. "github.com/intervention-engine/fhir/models"
  13. "github.com/intervention-engine/fhir/search"
  14. )
  15. // BatchController handles FHIR batch operations via input bundles
  16. type BatchController struct {
  17. DAL DataAccessLayer
  18. }
  19. // NewBatchController creates a new BatchController based on the passed in DAL
  20. func NewBatchController(dal DataAccessLayer) *BatchController {
  21. return &BatchController{DAL: dal}
  22. }
  23. // Post processes and incoming batch request
  24. func (b *BatchController) Post(c *gin.Context) {
  25. bundle := &models.Bundle{}
  26. err := FHIRBind(c, bundle)
  27. if err != nil {
  28. c.AbortWithError(http.StatusInternalServerError, err)
  29. return
  30. }
  31. // TODO: If type is batch, ensure there are no interdendent resources
  32. // Loop through the entries, ensuring they have a request and that we support the method,
  33. // while also creating a new entries array that can be sorted by method.
  34. entries := make([]*models.BundleEntryComponent, len(bundle.Entry))
  35. for i := range bundle.Entry {
  36. if bundle.Entry[i].Request == nil {
  37. c.AbortWithError(http.StatusBadRequest, errors.New("Entries in a batch operation require a request"))
  38. return
  39. }
  40. switch bundle.Entry[i].Request.Method {
  41. default:
  42. c.AbortWithError(http.StatusNotImplemented,
  43. errors.New("Operation currently unsupported in batch requests: "+bundle.Entry[i].Request.Method))
  44. return
  45. case "DELETE":
  46. if bundle.Entry[i].Request.Url == "" {
  47. c.AbortWithError(http.StatusBadRequest, errors.New("Batch DELETE must have a URL"))
  48. return
  49. }
  50. case "POST":
  51. if bundle.Entry[i].Resource == nil {
  52. c.AbortWithError(http.StatusBadRequest, errors.New("Batch POST must have a resource body"))
  53. return
  54. }
  55. case "PUT":
  56. if bundle.Entry[i].Resource == nil {
  57. c.AbortWithError(http.StatusBadRequest, errors.New("Batch PUT must have a resource body"))
  58. return
  59. }
  60. }
  61. entries[i] = &bundle.Entry[i]
  62. }
  63. sort.Sort(byRequestMethod(entries))
  64. // Now loop through the entries, assigning new IDs to those that are POST or Conditional PUT and fixing any
  65. // references to reference the new ID.
  66. refMap := make(map[string]models.Reference)
  67. newIDs := make([]string, len(entries))
  68. for i, entry := range entries {
  69. if entry.Request.Method == "POST" {
  70. // Create a new ID and add it to the reference map
  71. id := bson.NewObjectId().Hex()
  72. newIDs[i] = id
  73. refMap[entry.FullUrl] = models.Reference{
  74. Reference: entry.Request.Url + "/" + id,
  75. Type: entry.Request.Url,
  76. ReferencedID: id,
  77. External: new(bool),
  78. }
  79. // Rewrite the FullUrl using the new ID
  80. entry.FullUrl = responseURL(c.Request, entry.Request.Url, id).String()
  81. } else if entry.Request.Method == "PUT" && isConditional(entry) {
  82. // We need to process conditionals referencing temp IDs in a second pass, so skip them here
  83. if strings.Contains(entry.Request.Url, "urn:uuid:") {
  84. continue
  85. }
  86. if err := b.resolveConditionalPut(c.Request, i, entry, newIDs, refMap); err != nil {
  87. c.AbortWithError(http.StatusInternalServerError, err)
  88. return
  89. }
  90. }
  91. }
  92. // Second pass to take care of conditionals referencing temporary IDs. Known limitation: if a conditional
  93. // references a temp ID also defined by a conditional, we error out if it hasn't been resolved yet -- too many
  94. // rabbit holes.
  95. for i, entry := range entries {
  96. if entry.Request.Method == "PUT" && isConditional(entry) {
  97. // Use a regex to swap out the temp IDs with the new IDs
  98. for oldID, ref := range refMap {
  99. re := regexp.MustCompile("([=,])" + oldID + "(&|,|$)")
  100. entry.Request.Url = re.ReplaceAllString(entry.Request.Url, "${1}"+ref.Reference+"${2}")
  101. }
  102. if strings.Contains(entry.Request.Url, "urn:uuid:") {
  103. c.AbortWithError(http.StatusNotImplemented,
  104. errors.New("Cannot resolve conditionals referencing other conditionals"))
  105. return
  106. }
  107. if err := b.resolveConditionalPut(c.Request, i, entry, newIDs, refMap); err != nil {
  108. c.AbortWithError(http.StatusInternalServerError, err)
  109. return
  110. }
  111. }
  112. }
  113. // Update all the references to the entries (to reflect newly assigned IDs)
  114. updateAllReferences(entries, refMap)
  115. // Then make the changes in the database and update the entry response
  116. for i, entry := range entries {
  117. switch entry.Request.Method {
  118. case "DELETE":
  119. if !isConditional(entry) {
  120. // It's a normal DELETE
  121. parts := strings.SplitN(entry.Request.Url, "/", 2)
  122. if len(parts) != 2 {
  123. c.AbortWithError(http.StatusInternalServerError,
  124. fmt.Errorf("Couldn't identify resource and id to delete from %s", entry.Request.Url))
  125. return
  126. }
  127. if err := b.DAL.Delete(parts[1], parts[0]); err != nil && err != ErrNotFound {
  128. c.AbortWithError(http.StatusInternalServerError, err)
  129. return
  130. }
  131. } else {
  132. // It's a conditional (query-based) delete
  133. parts := strings.SplitN(entry.Request.Url, "?", 2)
  134. query := search.Query{Resource: parts[0], Query: parts[1]}
  135. if _, err := b.DAL.ConditionalDelete(query); err != nil {
  136. c.AbortWithError(http.StatusInternalServerError, err)
  137. return
  138. }
  139. }
  140. entry.Request = nil
  141. entry.Response = &models.BundleEntryResponseComponent{
  142. Status: "204",
  143. }
  144. case "POST":
  145. if err := b.DAL.PostWithID(newIDs[i], entry.Resource); err != nil {
  146. c.AbortWithError(http.StatusInternalServerError, err)
  147. return
  148. }
  149. entry.Request = nil
  150. entry.Response = &models.BundleEntryResponseComponent{
  151. Status: "201",
  152. Location: entry.FullUrl,
  153. }
  154. if meta, ok := models.GetResourceMeta(entry.Resource); ok {
  155. entry.Response.LastModified = meta.LastUpdated
  156. }
  157. case "PUT":
  158. // Because we pre-process conditional PUTs, we know this is always a normal PUT operation
  159. entry.FullUrl = responseURL(c.Request, entry.Request.Url).String()
  160. parts := strings.SplitN(entry.Request.Url, "/", 2)
  161. if len(parts) != 2 {
  162. c.AbortWithError(http.StatusInternalServerError,
  163. fmt.Errorf("Couldn't identify resource and id to put from %s", entry.Request.Url))
  164. return
  165. }
  166. createdNew, err := b.DAL.Put(parts[1], entry.Resource)
  167. if err != nil {
  168. c.AbortWithError(http.StatusInternalServerError, err)
  169. return
  170. }
  171. entry.Request = nil
  172. entry.Response = new(models.BundleEntryResponseComponent)
  173. entry.Response.Location = entry.FullUrl
  174. if createdNew {
  175. entry.Response.Status = "201"
  176. } else {
  177. entry.Response.Status = "200"
  178. }
  179. if meta, ok := models.GetResourceMeta(entry.Resource); ok {
  180. entry.Response.LastModified = meta.LastUpdated
  181. }
  182. }
  183. }
  184. total := uint32(len(entries))
  185. bundle.Total = &total
  186. bundle.Type = fmt.Sprintf("%s-response", bundle.Type)
  187. c.Set("Bundle", bundle)
  188. c.Set("Resource", "Bundle")
  189. c.Set("Action", "batch")
  190. // Send the response
  191. c.Header("Access-Control-Allow-Origin", "*")
  192. c.JSON(http.StatusOK, bundle)
  193. }
  194. func (b *BatchController) resolveConditionalPut(request *http.Request, entryIndex int, entry *models.BundleEntryComponent, newIDs []string, refMap map[string]models.Reference) error {
  195. // Do a preflight to either get the existing ID, get a new ID, or detect multiple matches (not allowed)
  196. parts := strings.SplitN(entry.Request.Url, "?", 2)
  197. query := search.Query{Resource: parts[0], Query: parts[1]}
  198. var id string
  199. if IDs, err := b.DAL.FindIDs(query); err == nil {
  200. switch len(IDs) {
  201. case 0:
  202. id = bson.NewObjectId().Hex()
  203. case 1:
  204. id = IDs[0]
  205. default:
  206. return ErrMultipleMatches
  207. }
  208. } else {
  209. return err
  210. }
  211. // Rewrite the PUT as a normal (non-conditional) PUT
  212. entry.Request.Url = query.Resource + "/" + id
  213. // Add the new ID to the reference map
  214. newIDs[entryIndex] = id
  215. refMap[entry.FullUrl] = models.Reference{
  216. Reference: entry.Request.Url,
  217. Type: query.Resource,
  218. ReferencedID: id,
  219. External: new(bool),
  220. }
  221. // Rewrite the FullUrl using the new ID
  222. entry.FullUrl = responseURL(request, entry.Request.Url, id).String()
  223. return nil
  224. }
  225. func updateAllReferences(entries []*models.BundleEntryComponent, refMap map[string]models.Reference) {
  226. // First, get all the references by reflecting through the fields of each model
  227. var refs []*models.Reference
  228. for _, entry := range entries {
  229. model := entry.Resource
  230. if model != nil {
  231. entryRefs := findRefsInValue(reflect.ValueOf(model))
  232. refs = append(refs, entryRefs...)
  233. }
  234. }
  235. // Then iterate through and update as necessary
  236. for _, ref := range refs {
  237. newRef, found := refMap[ref.Reference]
  238. if found {
  239. *ref = newRef
  240. }
  241. }
  242. }
  243. func findRefsInValue(val reflect.Value) []*models.Reference {
  244. var refs []*models.Reference
  245. // Dereference pointers in order to simplify things
  246. if val.Kind() == reflect.Ptr {
  247. val = val.Elem()
  248. }
  249. // Make sure it's a valid thing, else return right away
  250. if !val.IsValid() {
  251. return refs
  252. }
  253. // Handle it if it's a ref, otherwise iterate its members for refs
  254. if val.Type() == reflect.TypeOf(models.Reference{}) {
  255. refs = append(refs, val.Addr().Interface().(*models.Reference))
  256. } else if val.Kind() == reflect.Struct {
  257. for i := 0; i < val.NumField(); i++ {
  258. subRefs := findRefsInValue(val.Field(i))
  259. refs = append(refs, subRefs...)
  260. }
  261. } else if val.Kind() == reflect.Slice {
  262. for i := 0; i < val.Len(); i++ {
  263. subRefs := findRefsInValue(val.Index(i))
  264. refs = append(refs, subRefs...)
  265. }
  266. }
  267. return refs
  268. }
  269. func isConditional(entry *models.BundleEntryComponent) bool {
  270. if entry.Request == nil {
  271. return false
  272. } else if entry.Request.Method != "PUT" && entry.Request.Method != "DELETE" {
  273. return false
  274. }
  275. return !strings.Contains(entry.Request.Url, "/") || strings.Contains(entry.Request.Url, "?")
  276. }
  277. // Support sorting by request method, as defined in the spec
  278. type byRequestMethod []*models.BundleEntryComponent
  279. func (e byRequestMethod) Len() int {
  280. return len(e)
  281. }
  282. func (e byRequestMethod) Swap(i, j int) {
  283. e[i], e[j] = e[j], e[i]
  284. }
  285. func (e byRequestMethod) Less(i, j int) bool {
  286. methodMap := map[string]int{"DELETE": 0, "POST": 1, "PUT": 2, "GET": 3}
  287. return methodMap[e[i].Request.Method] < methodMap[e[j].Request.Method]
  288. }