Deliverable for D3.2

catalog.go 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package api
  2. import (
  3. "crypto/md5"
  4. "encoding/base64"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "strings"
  9. validator "gopkg.in/validator.v2"
  10. restful "github.com/emicklei/go-restful"
  11. restfulspec "github.com/emicklei/go-restful-openapi"
  12. uuid "github.com/satori/go.uuid"
  13. )
  14. // catalogResource is an 'in-memory' instance of a metadata service
  15. type catalogResource struct {
  16. store *MetadataStore
  17. }
  18. // ErrorResponse signals error messages back to the client
  19. type ErrorResponse struct {
  20. Error string `json:"error" description:"error message if any"`
  21. }
  22. // CatalogRequest contains the information required to register some data as being available at some location
  23. // The Tags property should contain enough information to enable a search index
  24. // The Sample property should contain enough detail to interact with the data
  25. type CatalogRequest struct {
  26. Key string `json:"key" description:"path of the data item" validate:"nonzero"`
  27. Tags []string `json:"tags" description:"a collection of tags probably belonging to an ontology" validate:"nonzero"`
  28. Sample string `json:"sample" description:"sample value e.g. a json object `
  29. }
  30. // CatalogItem contains the original request
  31. type CatalogItem struct {
  32. CatalogRequest
  33. UID string `json:"uid" description:"unique identifier for the catalogued piece of data" validate:"nonzero"`
  34. LocationUID string `json:"-"`
  35. }
  36. // ItemWithLocation contains the item metadata and its location
  37. type ItemWithLocation struct {
  38. CatalogItem
  39. Location Location `json:"location" description:"location for the catalogued piece of data" validate:"nonzero"`
  40. }
  41. // LocationRequest allows a node to register its presence with the service
  42. type LocationRequest struct {
  43. IPAddress string `json:"ip-address" description:"public IP address of the node" validate:"nonzero"`
  44. Port int `json:"port" description:"public port of the node" validate:"nonzero"`
  45. Scheme string `json:"scheme" description:protocol to use e.g. http or https`
  46. }
  47. // Location contains the original request and a UID to use when interacting with the service e.g. adding Items to the catalog.
  48. type Location struct {
  49. LocationRequest
  50. UID string `json:"uid" description:"unique identifier for a node" validate:"nonzero"`
  51. }
  52. func NewCatalogService(store *MetadataStore) catalogResource {
  53. return catalogResource{
  54. store: store,
  55. }
  56. }
  57. func (e catalogResource) WebService() *restful.WebService {
  58. ws := new(restful.WebService)
  59. ws.
  60. Path("/catalog").
  61. Consumes(restful.MIME_JSON).
  62. Produces(restful.MIME_JSON)
  63. catalogUIDParameter := ws.PathParameter("catalog-uid", "identifier for a cataloged item").DataType("string")
  64. locationUIDParameter := ws.PathParameter("location-uid", "identifier for a location").DataType("string")
  65. tags := []string{"metadata"}
  66. // register a node at a location
  67. ws.Route(ws.PUT("/announce").To(e.registerLocation).
  68. Doc("register a node's location").
  69. Metadata(restfulspec.KeyOpenAPITags, tags).
  70. Reads(LocationRequest{}).
  71. Returns(http.StatusOK, "OK", Location{}).
  72. Returns(http.StatusBadRequest, "error validating request", ErrorResponse{}).
  73. Returns(http.StatusInternalServerError, "something went wrong", ErrorResponse{}))
  74. // move a location
  75. ws.Route(ws.PATCH("/announce/{location-uid}").To(e.moveLocation).
  76. Param(locationUIDParameter).
  77. Doc("change a node's location - keeping the same location-uid").
  78. Metadata(restfulspec.KeyOpenAPITags, tags).
  79. Reads(LocationRequest{}).
  80. Returns(http.StatusOK, "OK", Location{}).
  81. Returns(http.StatusBadRequest, "error validating request", ErrorResponse{}).
  82. Returns(http.StatusNotFound, "Not found", nil).
  83. Returns(http.StatusInternalServerError, "something went wrong", ErrorResponse{}))
  84. // add an item to the catalog
  85. ws.Route(ws.PUT("/items/{location-uid}").To(e.catalogItem).
  86. Doc("catalog an item for discovery e.g. what and where").
  87. Param(locationUIDParameter).
  88. Metadata(restfulspec.KeyOpenAPITags, tags).
  89. Reads(CatalogRequest{}).
  90. Returns(http.StatusOK, "OK", CatalogItem{}).
  91. Returns(http.StatusBadRequest, "error validating request", ErrorResponse{}).
  92. Returns(http.StatusInternalServerError, "something went wrong", ErrorResponse{}))
  93. // delete an item from the catalog
  94. ws.Route(ws.DELETE("/items/{catalog-uid}").To(e.removeFromCatalog).
  95. Doc("delete an item from the catalog").
  96. Metadata(restfulspec.KeyOpenAPITags, tags).
  97. Returns(http.StatusOK, "OK", nil).
  98. Param(catalogUIDParameter))
  99. // get all items - simple search
  100. ws.Route(ws.GET("/items/").To(e.allItems).
  101. Doc("get all cataloged items").
  102. Metadata(restfulspec.KeyOpenAPITags, tags).
  103. Returns(http.StatusOK, "OK", []ItemWithLocation{}))
  104. return ws
  105. }
  106. func (e catalogResource) registerLocation(request *restful.Request, response *restful.Response) {
  107. req := LocationRequest{}
  108. if err := request.ReadEntity(&req); err != nil {
  109. response.WriteHeaderAndEntity(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
  110. return
  111. }
  112. if errs := validator.Validate(req); errs != nil {
  113. response.WriteHeaderAndEntity(http.StatusBadRequest, ErrorResponse{Error: errs.Error()})
  114. return
  115. }
  116. location := Location{
  117. LocationRequest: req,
  118. UID: uuid.NewV4().String(),
  119. }
  120. e.store.Locations.Add(location)
  121. log.Print("node registered :", location)
  122. response.WriteEntity(location)
  123. }
  124. func (e catalogResource) moveLocation(request *restful.Request, response *restful.Response) {
  125. locationUID := request.PathParameter("location-uid")
  126. req := LocationRequest{}
  127. if err := request.ReadEntity(&req); err != nil {
  128. response.WriteHeaderAndEntity(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
  129. return
  130. }
  131. if errs := validator.Validate(req); errs != nil {
  132. response.WriteHeaderAndEntity(http.StatusBadRequest, ErrorResponse{Error: errs.Error()})
  133. return
  134. }
  135. location := Location{
  136. LocationRequest: req,
  137. UID: locationUID,
  138. }
  139. err := e.store.Locations.Replace(locationUID, location)
  140. if err != nil {
  141. if err == ErrLocationNotExists {
  142. response.WriteHeader(http.StatusNotFound)
  143. return
  144. }
  145. response.WriteHeaderAndEntity(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
  146. return
  147. }
  148. log.Print("node moved :", location)
  149. response.WriteEntity(location)
  150. }
  151. func (e catalogResource) catalogItem(request *restful.Request, response *restful.Response) {
  152. req := CatalogRequest{}
  153. if err := request.ReadEntity(&req); err != nil {
  154. response.WriteHeaderAndEntity(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
  155. return
  156. }
  157. if errs := validator.Validate(req); errs != nil {
  158. response.WriteHeaderAndEntity(http.StatusBadRequest, ErrorResponse{Error: errs.Error()})
  159. return
  160. }
  161. locationUID := request.PathParameter("location-uid")
  162. found := e.store.Locations.Exists(locationUID)
  163. if !found {
  164. response.WriteHeaderAndEntity(http.StatusInternalServerError, ErrorResponse{Error: "unknown node"})
  165. return
  166. }
  167. // generate a consistent uid
  168. key := []byte(fmt.Sprintf("%s:%s", locationUID, req.Key))
  169. hash := md5.Sum(key)
  170. encoded := base64.StdEncoding.EncodeToString(hash[:])
  171. // urlencode the url as used in URL for deletes
  172. encoded = strings.Replace(encoded, "/", "_", -1)
  173. item := CatalogItem{
  174. CatalogRequest: req,
  175. UID: encoded,
  176. LocationUID: locationUID,
  177. }
  178. e.store.Items.Add(item)
  179. response.WriteEntity(item)
  180. }
  181. func (e catalogResource) removeFromCatalog(request *restful.Request, response *restful.Response) {
  182. uid := request.PathParameter("catalog-uid")
  183. e.store.Items.Delete(uid)
  184. }
  185. func (e catalogResource) allItems(request *restful.Request, response *restful.Response) {
  186. list := e.store.All()
  187. response.WriteEntity(list)
  188. }