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  }