github.com/go-spatial/go-wfs@v0.1.4-0.20190401000911-c9fba2bb5188/server/handlers.go (about) 1 /////////////////////////////////////////////////////////////////////////////// 2 // 3 // The MIT License (MIT) 4 // Copyright (c) 2018 Jivan Amara 5 // Copyright (c) 2018 Tom Kralidis 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to 9 // deal in the Software without restriction, including without limitation the 10 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 // sell copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 // USE OR OTHER DEALINGS IN THE SOFTWARE. 24 // 25 /////////////////////////////////////////////////////////////////////////////// 26 27 package server 28 29 import ( 30 "bytes" 31 "encoding/json" 32 "fmt" 33 "io/ioutil" 34 "log" 35 "net/http" 36 "net/url" 37 "strconv" 38 "strings" 39 40 "github.com/go-spatial/geom" 41 "github.com/go-spatial/jivan/config" 42 "github.com/go-spatial/jivan/data_provider" 43 "github.com/go-spatial/jivan/wfs3" 44 "github.com/julienschmidt/httprouter" 45 ) 46 47 // This is the default max number of features to return for feature collection reqeusts 48 const DEFAULT_RESULT_LIMIT = 10 49 50 const ( 51 HTTPStatusOk = 200 52 HTTPStatusNotModified = 304 53 HTTPStatusServerError = 500 54 HTTPStatusClientError = 400 55 56 HTTPMethodGET = "GET" 57 HTTPMethodHEAD = "HEAD" 58 ) 59 60 type HandlerError struct { 61 Code string `json:"code"` 62 Description string `json:"description"` 63 } 64 65 // contentType() returns the Content-Type string that will be used for the response to this request. 66 // This Content-Type will be chosen in order of increasing priority from: 67 // request Content-Type, request Accept 68 // If the type chosen from the request isn't supported, defaultContentType will be used. 69 func supportedContentType(ct string) bool { 70 supportedContentTypes := []string{config.JSONContentType, config.HTMLContentType} 71 typeSupported := false 72 for _, sct := range supportedContentTypes { 73 if ct == sct { 74 typeSupported = true 75 break 76 } 77 } 78 return typeSupported 79 } 80 81 func contentType(r *http.Request) string { 82 defaultContentType := config.JSONContentType 83 useType := "" 84 ctType := r.Header.Get("Content-Type") 85 acceptTypes := r.Header.Get("Accept") 86 87 if supportedContentType(ctType) { 88 useType = ctType 89 } 90 91 // TODO: Parse acceptTypes properly 92 acceptTypes = acceptTypes 93 94 // if query string 'f' parameter is passed 95 // override HTTP Accept header 96 q := r.URL.Query() 97 qFormat := q["f"] 98 99 if len(qFormat) > 0 { 100 if qFormat[0] != useType { 101 useType = qFormat[0] 102 } 103 } 104 105 if !supportedContentType(useType) { 106 useType = defaultContentType 107 } 108 109 return useType 110 } 111 112 // Sets response 'status', and writes a json-encoded object with property "description" having value "msg". 113 func jsonError(w http.ResponseWriter, code string, msg string, status int) { 114 w.WriteHeader(status) 115 116 result, err := json.Marshal(struct { 117 Code string `json:"code"` 118 Description string `json:"description"` 119 }{ 120 Code: code, 121 Description: msg, 122 }) 123 124 if err != nil { 125 w.Write([]byte(fmt.Sprintf("problem marshaling error: %v", msg))) 126 } else { 127 w.Write(result) 128 } 129 } 130 131 // Provides a link for the given content type 132 func ctLink(baselink, contentType string) string { 133 if !supportedContentType(contentType) { 134 panic(fmt.Sprintf("unsupported content type: %v", contentType)) 135 } 136 137 u, err := url.Parse(baselink) 138 if err != nil { 139 log.Printf("Invalid link '%v', will return empty string.", baselink) 140 return "" 141 } 142 q := u.Query() 143 144 var l string 145 switch contentType { 146 case config.Configuration.Server.DefaultMimeType: 147 default: 148 q["f"] = []string{contentType} 149 } 150 151 u.RawQuery = q.Encode() 152 l = u.String() 153 return l 154 } 155 156 // Serves the root content for WFS3. 157 func root(w http.ResponseWriter, r *http.Request) { 158 ct := contentType(r) 159 rPath := "/" 160 // This allows tests to set the result to whatever they want. 161 overrideContent := r.Context().Value("overrideContent") 162 163 rootContent, contentId := wfs3.Root(false) 164 165 sshpb := serveSchemeHostPortBase(r) 166 apiUrl := fmt.Sprintf("%v/api", sshpb) 167 conformanceUrl := fmt.Sprintf("%v/conformance", sshpb) 168 collectionsUrl := fmt.Sprintf("%v/collections", sshpb) 169 rootUrl := fmt.Sprintf("%v/", sshpb) 170 171 alttypes := []string{} 172 switch ct { 173 case config.JSONContentType: 174 alttypes = append(alttypes, config.HTMLContentType) 175 case config.HTMLContentType: 176 alttypes = append(alttypes, config.JSONContentType) 177 } 178 179 var links []*wfs3.Link 180 links = append(links, &wfs3.Link{Href: ctLink(rootUrl, ct), Rel: "self", Type: ct}) 181 for _, at := range alttypes { 182 links = append(links, &wfs3.Link{Href: ctLink(rootUrl, at), Rel: "alternate", Type: at}) 183 } 184 links = append(links, &wfs3.Link{Href: ctLink(apiUrl, ct), Rel: "service", Type: ct}) 185 links = append(links, &wfs3.Link{Href: ctLink(conformanceUrl, ct), Rel: "conformance", Type: ct}) 186 links = append(links, &wfs3.Link{Href: ctLink(collectionsUrl, ct), Rel: "data", Type: ct}) 187 188 rootContent.Links = links 189 190 w.Header().Set("ETag", contentId) 191 if r.Method == HTTPMethodHEAD { 192 if r.Header.Get("ETag") == contentId { 193 w.WriteHeader(HTTPStatusNotModified) 194 } else { 195 w.WriteHeader(HTTPStatusOk) 196 } 197 return 198 } 199 200 var encodedContent []byte 201 var err error 202 if ct == config.JSONContentType { 203 encodedContent, err = json.Marshal(rootContent) 204 } else if ct == config.HTMLContentType { 205 encodedContent, err = rootContent.MarshalHTML(config.Configuration) 206 } else { 207 jsonError(w, "InvalidParameterValue", "Content-Type: '"+ct+"' not supported.", HTTPStatusServerError) 208 return 209 } 210 211 if err != nil { 212 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 213 return 214 } 215 216 w.Header().Set("Content-Type", ct) 217 218 if overrideContent != nil { 219 encodedContent = overrideContent.([]byte) 220 } 221 222 if ct == config.JSONContentType { 223 respBodyRC := ioutil.NopCloser(bytes.NewReader(encodedContent)) 224 err = wfs3.ValidateJSONResponse(r, rPath, HTTPStatusOk, w.Header(), respBodyRC) 225 if err != nil { 226 log.Printf("%v", err) 227 jsonError(w, "NoApplicableCode", "response doesn't match schema", HTTPStatusServerError) 228 return 229 } 230 } 231 232 w.WriteHeader(HTTPStatusOk) 233 w.Write(encodedContent) 234 } 235 236 func conformance(w http.ResponseWriter, r *http.Request) { 237 cPath := "/conformance" 238 // This allows tests to set the result to whatever they want. 239 overrideContent := r.Context().Value("overrideContent") 240 241 ct := contentType(r) 242 c, contentId := wfs3.Conformance() 243 w.Header().Set("ETag", contentId) 244 if r.Method == HTTPMethodHEAD { 245 if r.Header.Get("ETag") == contentId { 246 w.WriteHeader(HTTPStatusNotModified) 247 } else { 248 w.WriteHeader(HTTPStatusOk) 249 } 250 return 251 } 252 253 var encodedContent []byte 254 var err error 255 if ct == config.JSONContentType { 256 encodedContent, err = json.Marshal(c) 257 } else if ct == config.HTMLContentType { 258 encodedContent, err = c.MarshalHTML(config.Configuration) 259 } else { 260 jsonError(w, "InvalidParameterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 261 return 262 } 263 264 if err != nil { 265 msg := fmt.Sprintf("problem marshaling conformance declaration to %v: %v", ct, err.Error()) 266 jsonError(w, "NoApplicableCode", msg, HTTPStatusServerError) 267 return 268 } 269 270 w.Header().Set("Content-Type", ct) 271 272 if overrideContent != nil { 273 encodedContent = overrideContent.([]byte) 274 } 275 if ct == config.JSONContentType { 276 respBodyRC := ioutil.NopCloser(bytes.NewReader(encodedContent)) 277 err = wfs3.ValidateJSONResponse(r, cPath, HTTPStatusOk, w.Header(), respBodyRC) 278 if err != nil { 279 log.Printf(fmt.Sprintf("%v", err)) 280 jsonError(w, "NoApplicableCode", "response doesn't match schema", HTTPStatusServerError) 281 return 282 } 283 } 284 285 w.WriteHeader(HTTPStatusOk) 286 w.Write(encodedContent) 287 } 288 289 // --- Return the json-encoded OpenAPI 3 spec for the WFS API available on this instance. 290 func openapi(w http.ResponseWriter, r *http.Request) { 291 // --- TODO: Disabled due to #34 292 // oapiPath := "/api" 293 // This allows tests to set the result to whatever they want. 294 overrideContent := r.Context().Value("overrideContent") 295 296 ct := contentType(r) 297 298 if ct != config.JSONContentType { 299 jsonError(w, "InvalidParameterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 300 return 301 } 302 encodedContent, contentId := wfs3.OpenAPI3SchemaEncoded(ct) 303 w.Header().Set("ETag", contentId) 304 305 if r.Method == HTTPMethodHEAD { 306 if r.Header.Get("ETag") == contentId { 307 w.WriteHeader(HTTPStatusNotModified) 308 } else { 309 w.WriteHeader(HTTPStatusOk) 310 } 311 return 312 } 313 314 w.Header().Set("Content-Type", ct) 315 316 if overrideContent != nil { 317 encodedContent = overrideContent.([]byte) 318 } 319 320 // TODO: As of 2018-04-05 I can't find a reliable openapi3 document schema. When one is published use if for validation here. 321 // if ct == config.JSONContentType { 322 // err := wfs3.ValidateJSONResponseAgainstJSONSchema(encodedContent, jsonSchema) 323 // if err != nil { 324 // log.Printf(fmt.Sprintf("%v", err)) 325 // jsonError(w, "NoApplicableCode", "response doesn't match schema", HTTPStatusServerError) 326 // return 327 // } 328 // } else { 329 // msg := fmt.Sprintf("unsupported content type: %v", ct) 330 // log.Printf(msg) 331 // jsonError(w, "InvalidParametrValue", msg, HTTPStatusClientError) 332 // } 333 334 w.WriteHeader(HTTPStatusOk) 335 w.Write(encodedContent) 336 } 337 338 func collectionMetaData(w http.ResponseWriter, r *http.Request) { 339 cmdPath := "/collections/{name}" 340 overrideContent := r.Context().Value("overrideContent") 341 342 ct := contentType(r) 343 ps := httprouter.ParamsFromContext(r.Context()) 344 345 cName := ps.ByName("name") 346 if cName == "" { 347 jsonError(w, "MissingParameterValue", "No {name} provided", HTTPStatusClientError) 348 return 349 } 350 351 md, contentId, err := wfs3.CollectionMetaData(cName, &Provider, serveSchemeHostPortBase(r), false) 352 if err != nil { 353 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 354 return 355 } 356 357 collectionMdUrlBase := fmt.Sprintf("%v/collections/%v", serveSchemeHostPortBase(r), cName) 358 collectionDataUrlBase := fmt.Sprintf("%v/collections/%v/items", serveSchemeHostPortBase(r), cName) 359 altcts := []string{} 360 switch ct { 361 case config.JSONContentType: 362 altcts = append(altcts, config.HTMLContentType) 363 case config.HTMLContentType: 364 altcts = append(altcts, config.JSONContentType) 365 default: 366 jsonError(w, "InvalidParamaterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 367 } 368 // Prepend these self-pointing links to md.Links 369 plinks := []*wfs3.Link{} 370 plinks = append(plinks, &wfs3.Link{Rel: "self", Href: ctLink(collectionMdUrlBase, ct), Type: ct}) 371 for _, act := range altcts { 372 plinks = append(plinks, &wfs3.Link{Rel: "alternate", Href: ctLink(collectionMdUrlBase, act), Type: act}) 373 } 374 // Include these links to actual data 375 plinks = append(plinks, &wfs3.Link{Rel: "item", Href: ctLink(collectionDataUrlBase, ct), Type: ct}) 376 for _, act := range altcts { 377 plinks = append(plinks, &wfs3.Link{Rel: "item", Href: ctLink(collectionDataUrlBase, act), Type: act}) 378 } 379 md.Links = append(plinks, md.Links...) 380 381 w.Header().Set("ETag", contentId) 382 if r.Method == HTTPMethodHEAD { 383 if r.Header.Get("ETag") == contentId { 384 w.WriteHeader(HTTPStatusNotModified) 385 } else { 386 w.WriteHeader(HTTPStatusOk) 387 } 388 return 389 } 390 391 var encodedContent []byte 392 if ct == config.JSONContentType { 393 md.ContentType(ct) 394 encodedContent, err = json.Marshal(md) 395 } else if ct == config.HTMLContentType { 396 encodedContent, err = md.MarshalHTML(config.Configuration) 397 } else { 398 jsonError(w, "InvalidParamaterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 399 return 400 } 401 402 if err != nil { 403 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 404 return 405 } 406 407 w.Header().Set("Content-Type", ct) 408 409 if overrideContent != nil { 410 encodedContent = overrideContent.([]byte) 411 } 412 413 if ct == config.JSONContentType { 414 respBodyRC := ioutil.NopCloser(bytes.NewReader(encodedContent)) 415 err = wfs3.ValidateJSONResponse(r, cmdPath, HTTPStatusOk, w.Header(), respBodyRC) 416 if err != nil { 417 log.Printf(fmt.Sprintf("%v", err)) 418 jsonError(w, "NoApplicableCode", "response doesn't match schema", HTTPStatusServerError) 419 return 420 } 421 } 422 423 w.WriteHeader(HTTPStatusOk) 424 w.Write(encodedContent) 425 } 426 427 func collectionsMetaData(w http.ResponseWriter, r *http.Request) { 428 cmdPath := "/collections" 429 overrideContent := r.Context().Value("overrideContent") 430 431 ct := contentType(r) 432 md, contentId, err := wfs3.CollectionsMetaData(&Provider, serveSchemeHostPortBase(r), false) 433 if err != nil { 434 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 435 return 436 } 437 438 w.Header().Set("ETag", contentId) 439 if r.Method == HTTPMethodHEAD { 440 if r.Header.Get("ETag") == contentId { 441 w.WriteHeader(HTTPStatusNotModified) 442 } else { 443 w.WriteHeader(HTTPStatusOk) 444 } 445 return 446 } 447 448 // This needs to be done before adding the alternate links below, otherwise they will all be 449 // converted to ct 450 md.ContentType(ct) 451 452 // Add self link to beginning of Links 453 selfHrefBase := fmt.Sprintf("%v%v", serveSchemeHostPortBase(r), cmdPath) 454 selfLink := &wfs3.Link{Rel: "self", Href: ctLink(selfHrefBase, ct), Type: ct} 455 456 // Add alternative links after self link 457 altLinks := make([]*wfs3.Link, 0, 5) 458 for _, sct := range config.SupportedContentTypes { 459 if ct == sct { 460 continue 461 } 462 altLinks = append(altLinks, &wfs3.Link{Rel: "alternate", Href: ctLink(selfHrefBase, sct), Type: sct}) 463 } 464 465 // Add item links after alt links 466 ilinks := make([]*wfs3.Link, 0, len(md.Collections)) 467 for _, c := range md.Collections { 468 chref := fmt.Sprintf("%v/%v", selfHrefBase, c.Name) 469 // self & alternate links 470 c.Links = append(c.Links, &wfs3.Link{Rel: "self", Href: ctLink(chref, ct), Type: ct}) 471 ilinks = append(ilinks, &wfs3.Link{Rel: "item", Href: ctLink(chref, ct), Type: ct}) 472 for _, sct := range config.SupportedContentTypes { 473 if ct == sct { 474 continue 475 } 476 c.Links = append(c.Links, &wfs3.Link{Rel: "alternate", Href: ctLink(chref, sct), Type: sct}) 477 ilinks = append(ilinks, &wfs3.Link{Rel: "item", Href: ctLink(chref, sct), Type: sct}) 478 } 479 // item links 480 ihref := fmt.Sprintf("%v/%v/items", selfHrefBase, c.Name) 481 for _, sct := range config.SupportedContentTypes { 482 c.Links = append(c.Links, &wfs3.Link{Rel: "item", Href: ctLink(ihref, sct), Type: sct}) 483 } 484 } 485 links := []*wfs3.Link{selfLink} 486 links = append(links, altLinks...) 487 links = append(links, md.Links...) 488 links = append(links, ilinks...) 489 md.Links = links 490 491 var encodedContent []byte 492 if ct == config.JSONContentType { 493 encodedContent, err = json.Marshal(md) 494 } else if ct == config.HTMLContentType { 495 encodedContent, err = md.MarshalHTML(config.Configuration) 496 } else { 497 jsonError(w, "InvalidParameterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 498 return 499 } 500 501 if err != nil { 502 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 503 return 504 } 505 506 w.Header().Set("Content-Type", ct) 507 508 if overrideContent != nil { 509 encodedContent = overrideContent.([]byte) 510 } 511 512 if ct == config.JSONContentType { 513 respBodyRC := ioutil.NopCloser(bytes.NewReader(encodedContent)) 514 err = wfs3.ValidateJSONResponse(r, cmdPath, HTTPStatusOk, w.Header(), respBodyRC) 515 if err != nil { 516 log.Printf(fmt.Sprintf("%v", err)) 517 jsonError(w, "NoApplicableCode", "response doesn't match schema", HTTPStatusServerError) 518 return 519 } 520 } 521 522 w.WriteHeader(HTTPStatusOk) 523 w.Write(encodedContent) 524 } 525 526 // --- Provide paged access to data for all features at /collections/{name}/items/{feature_id} 527 func collectionData(w http.ResponseWriter, r *http.Request) { 528 ct := contentType(r) 529 overrideContent := r.Context().Value("overrideContent") 530 531 urlParams := httprouter.ParamsFromContext(r.Context()) 532 cName := urlParams.ByName("name") 533 fidStr := urlParams.ByName("feature_id") 534 var fid uint64 535 var err error 536 if fidStr != "" { 537 cid, err := strconv.Atoi(fidStr) 538 if err != nil { 539 jsonError(w, "InvalidParameterValue", "Invalid feature_id: "+fidStr, HTTPStatusClientError) 540 } 541 fid = uint64(cid) 542 } 543 544 q := r.URL.Query() 545 reservedQParams := []string{"f", "page", "limit", "time", "bbox"} 546 var limit, pageNum uint 547 var timeprops map[string]string 548 549 qPageSize := q["limit"] 550 if len(qPageSize) != 1 { 551 limit = DEFAULT_RESULT_LIMIT 552 } else { 553 ps, err := strconv.ParseUint(qPageSize[0], 10, 64) 554 if err != nil { 555 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusClientError) 556 return 557 } 558 if ps > uint64(config.Configuration.Server.MaxLimit) { 559 ps = uint64(config.Configuration.Server.MaxLimit) 560 } 561 limit = uint(ps) 562 } 563 564 qPageNum := q["page"] 565 if len(qPageNum) != 1 { 566 pageNum = 0 567 } else { 568 pn, err := strconv.ParseUint(qPageNum[0], 10, 64) 569 if err != nil { 570 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusClientError) 571 return 572 } 573 pageNum = uint(pn) 574 } 575 576 qBBox := q["bbox"] 577 var bbox *geom.Extent 578 if len(qBBox) > 0 { 579 if len(qBBox) > 1 { 580 jsonError(w, "InvalidParameterValue", "'bbox' parameter provided more than once", HTTPStatusClientError) 581 return 582 } 583 584 bbox_items := strings.Split(qBBox[0], ",") 585 if len(bbox_items) != 4 { 586 msg := fmt.Sprintf("'bbox' parameter has %v items, expecting 4: '%v'", len(bbox_items), qBBox[0]) 587 jsonError(w, "InvalidParameterValue", msg, HTTPStatusClientError) 588 return 589 } else { 590 bbox = &geom.Extent{} 591 for i, p := range bbox_items { 592 if bbox[i], err = strconv.ParseFloat(p, 64); err != nil { 593 msg := fmt.Sprintf("'bbox' parameter has invalid format for item %v/4: '%v' / '%v'", i+1, p, qBBox[0]) 594 jsonError(w, "InvalidParameterValue", msg, HTTPStatusClientError) 595 return 596 } 597 } 598 } 599 } 600 601 qTime := q["time"] 602 if len(qTime) > 0 { 603 if len(qTime) > 1 { 604 jsonError(w, "InvalidParameterValue", "'time' parameter provided more than once'", HTTPStatusClientError) 605 return 606 } 607 ts := strings.Split(qTime[0], "/") 608 timeprops = make(map[string]string) 609 if len(ts) == 1 { 610 timeprops["timestamp"] = ts[0] 611 } else if len(ts) == 2 { 612 timeprops["start_time"] = ts[0] 613 timeprops["stop_time"] = ts[1] 614 } else { 615 jsonError(w, "InvalidParameterValue", "'time' parameter contains more than two time values ('/' separator)", HTTPStatusClientError) 616 return 617 } 618 } 619 620 // Collect additional property filters 621 properties := make(map[string]string) 622 NEXT_QUERY_PARAM: 623 for k, v := range q { 624 for _, rqp := range reservedQParams { 625 if k == rqp { 626 continue NEXT_QUERY_PARAM 627 } 628 } 629 630 properties[k] = v[0] 631 } 632 633 // Add time-specific properties 634 for k, v := range timeprops { 635 properties[k] = v 636 } 637 638 var data interface{} 639 var jsonSchema string 640 // Hex string hash of content 641 var contentId string 642 // Indicates if there is more data available from stopIdx onward 643 var featureTotal uint 644 // If a feature_id was provided, get a single feature, otherwise get a feature collection 645 // containing all of the collection's features 646 if fidStr != "" { 647 data, contentId, err = wfs3.FeatureData(cName, fid, &Provider, false) 648 jsonSchema = wfs3.FeatureJSONSchema 649 } else { 650 // First index we're interested in 651 startIdx := limit * pageNum 652 // Last index we're interested in +1 653 stopIdx := startIdx + limit 654 655 data, featureTotal, contentId, err = wfs3.FeatureCollectionData(cName, bbox, startIdx, stopIdx, properties, &Provider, false) 656 jsonSchema = wfs3.FeatureCollectionJSONSchema 657 } 658 659 if err != nil { 660 var sc int 661 var msg string 662 switch e := err.(type) { 663 case *data_provider.BadTimeString: 664 msg = e.Error() 665 sc = HTTPStatusClientError 666 default: 667 msg = fmt.Sprintf("Problem collecting feature data: %v", e) 668 sc = HTTPStatusServerError 669 } 670 jsonError(w, "InvalidParameterValue", msg, sc) 671 return 672 } 673 674 w.Header().Set("ETag", contentId) 675 if r.Method == HTTPMethodHEAD { 676 if r.Header.Get("ETag") == contentId { 677 w.WriteHeader(HTTPStatusNotModified) 678 } else { 679 w.WriteHeader(HTTPStatusOk) 680 } 681 return 682 } 683 684 // Alternate content types 685 var altcts []string 686 switch ct { 687 case config.JSONContentType: 688 altcts = append(altcts, config.HTMLContentType) 689 case config.HTMLContentType: 690 altcts = append(altcts, config.JSONContentType) 691 } 692 693 var encodedContent []byte 694 switch d := data.(type) { 695 case *wfs3.Feature: 696 // Generate links 697 shref := fmt.Sprintf("%v/collections/%v/items/%v", serveSchemeHostPortBase(r), cName, fid) 698 for _, sct := range config.SupportedContentTypes { 699 rel := "alternate" 700 if sct == ct { 701 rel = "self" 702 } 703 d.Links = append(d.Links, &wfs3.Link{Rel: rel, Href: ctLink(shref, sct), Type: sct}) 704 } 705 chref := fmt.Sprintf("%v/collections/%v", serveSchemeHostPortBase(r), cName) 706 d.Links = append(d.Links, &wfs3.Link{Rel: "collection", Href: ctLink(chref, ct), Type: ct}) 707 708 if ct == config.JSONContentType { 709 encodedContent, err = json.Marshal(d) 710 } else if ct == config.HTMLContentType { 711 encodedContent, err = d.MarshalHTML(config.Configuration) 712 } else { 713 jsonError(w, "InvalidParameterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 714 return 715 } 716 case *wfs3.FeatureCollection: 717 // Generate self, previous, and next links 718 self := fmt.Sprintf( 719 "%v/collections/%v/items?page=%v&limit=%v", serveSchemeHostPortBase(r), cName, pageNum, limit) 720 var prev string 721 var next string 722 if pageNum > 0 { 723 prev = fmt.Sprintf( 724 "%v/collections/%v/items?page=%v&limit=%v", serveSchemeHostPortBase(r), cName, pageNum-1, limit) 725 purl, err := url.Parse(prev) 726 if err != nil { 727 jsonError(w, "NoApplicableCode", "problem parsing generated 'prev' link", 500) 728 return 729 } 730 purl.RawQuery = purl.Query().Encode() 731 prev = purl.String() 732 } 733 if featureTotal > (limit * (pageNum + 1)) { 734 next = fmt.Sprintf( 735 "%v/collections/%v/items?page=%v&limit=%v", serveSchemeHostPortBase(r), cName, pageNum+1, limit) 736 nurl, err := url.Parse(next) 737 if err != nil { 738 jsonError(w, "NoApplicableCode", "problem parsing generated 'next' link", 500) 739 return 740 } 741 nurl.RawQuery = nurl.Query().Encode() 742 next = nurl.String() 743 } 744 745 d.Links = append(d.Links, &wfs3.Link{Rel: "self", Href: ctLink(self, ct), Type: ct}) 746 var alts = []*wfs3.Link{} 747 for _, act := range altcts { 748 alts = append(alts, &wfs3.Link{Rel: "alternate", Href: ctLink(self, act), Type: act}) 749 } 750 d.Links = append(d.Links, alts...) 751 if prev != "" { 752 d.Links = append(d.Links, &wfs3.Link{Rel: "prev", Href: prev, Type: ct}) 753 } 754 if next != "" { 755 d.Links = append(d.Links, &wfs3.Link{Rel: "next", Href: next, Type: ct}) 756 } 757 d.NumberMatched = featureTotal 758 d.NumberReturned = uint(len(d.Features)) 759 760 if ct == config.JSONContentType { 761 encodedContent, err = json.Marshal(d) 762 } else if ct == config.HTMLContentType { 763 encodedContent, err = d.MarshalHTML(config.Configuration) 764 } else { 765 jsonError(w, "InvalidParameterValue", "Content-Type: ''"+ct+"'' not supported.", HTTPStatusServerError) 766 return 767 } 768 default: 769 msg := fmt.Sprintf("Unexpected feature data type: %T, %v", data, data) 770 jsonError(w, "NoApplicableCode", msg, HTTPStatusServerError) 771 return 772 } 773 774 if err != nil { 775 msg := fmt.Sprintf("Problem marshalling feature data: %v", err) 776 jsonError(w, "oApplicableCode", msg, HTTPStatusServerError) 777 } 778 779 w.Header().Set("Content-Type", ct) 780 781 if overrideContent != nil { 782 encodedContent = overrideContent.([]byte) 783 } 784 785 if ct == config.JSONContentType { 786 err = wfs3.ValidateJSONResponseAgainstJSONSchema(encodedContent, jsonSchema) 787 if err != nil { 788 log.Printf(fmt.Sprintf("%v", err)) 789 jsonError(w, "NoApplicableCode", "response doesn't match schema", HTTPStatusServerError) 790 return 791 } 792 } 793 794 w.WriteHeader(HTTPStatusOk) 795 w.Write(encodedContent) 796 } 797 798 // --- Create temporary collection w/ filtered features. 799 // Returns a collection id for inspecting the resulting features. 800 func filteredFeatures(w http.ResponseWriter, r *http.Request) { 801 q := r.URL.Query() 802 extentParam := q["extent"] 803 collectionParam := q["collection"] 804 805 // Grab any params besides "extent" & "collection" as property filters. 806 propParams := make(map[string]string, len(q)) 807 for k, v := range r.URL.Query() { 808 if k == "extent" || k == "collection" { 809 continue 810 } 811 propParams[k] = v[0] 812 if len(v) > 1 { 813 log.Printf("Got multiple values for property filter, will only use the first '%v': %v", k, v) 814 } 815 } 816 817 var collectionNames []string 818 if len(collectionParam) > 0 { 819 collectionNames = collectionParam 820 } else { 821 var err error 822 collectionNames, err = Provider.CollectionNames() 823 if err != nil { 824 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 825 } 826 } 827 828 var extent geom.Extent 829 if len(extentParam) > 0 { 830 // lat/lon bounding box arranged as [<minx>, <miny>, <maxx>, <maxy>] 831 var llbbox [4]float64 832 err := json.Unmarshal([]byte(extentParam[0]), &llbbox) 833 if err != nil { 834 jsonError(w, "NoApplicableCode", fmt.Sprintf("unable to unmarshal extent (%v) due to error: %v", extentParam[0], err), HTTPStatusClientError) 835 return 836 } 837 extent = geom.Extent{llbbox[0], llbbox[1], llbbox[2], llbbox[3]} 838 // TODO: filter by extent 839 if len(extentParam) > 1 { 840 log.Printf("Multiple extent filters, will only use the first '%v'", extentParam) 841 } 842 } 843 844 fids, err := Provider.FilterFeatures(&extent, collectionNames, propParams) 845 newCol, err := Provider.MakeCollection("tempcol", fids) 846 847 if err != nil { 848 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 849 return 850 } 851 852 resp, err := json.Marshal(struct { 853 Collection string 854 FeatureCount int 855 }{Collection: newCol, FeatureCount: len(fids)}) 856 if err != nil { 857 jsonError(w, "NoApplicableCode", err.Error(), HTTPStatusServerError) 858 } 859 w.WriteHeader(HTTPStatusOk) 860 w.Write(resp) 861 }