view pkg/wfs/capabilities.go @ 4606:dfe9cde6a20c geoserver_sql_views

Reflect database model changes for SQL views in backend In principle, we could use many datasources with different database schemas, but this would imply changing GeoServer initialization, service filtering, endpoints and eventually more. Since we do not need it, just hard-code the schema name as a constant.
author Tom Gottfried <tom@intevation.de>
date Thu, 05 Sep 2019 12:23:31 +0200
parents 04876d865528
children
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package wfs

import (
	"encoding/xml"
	"errors"
	"io"
	"regexp"
	"strconv"
	"strings"

	"golang.org/x/net/html/charset"
)

// Keyword stores a value.
type Keyword struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Keyword"`
	Value   string   `xml:",cdata"`
}

// Keywords stores a list of keywords.
type Keywords struct {
	XMLName  xml.Name  `xml:"http://www.opengis.net/ows/1.1 Keywords"`
	Keywords []Keyword `xml:"Keyword"`
}

// ServiceIdentification contains meta informations about a WFS.
type ServiceIdentification struct {
	XMLName            xml.Name `xml:"http://www.opengis.net/ows/1.1 ServiceIdentification"`
	Title              string
	Abstract           string
	Keywords           Keywords `xml:"Keywords"`
	ServiceType        string
	ServiceTypeVersion string
}

// Get stores the link to the GET method
type Get struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Get"`
	HRef    string   `xml:"http://www.w3.org/1999/xlink href,attr"`
}

// Post stores the link to the POST method.
type Post struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Post"`
	HRef    string   `xml:"http://www.w3.org/1999/xlink href,attr"`
}

// HTTP is a container for HTTP methods.
type HTTP struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 HTTP"`
	Get     *Get     `xml:"Get"`
	Post    *Post    `xml:"Post"`
}

// DCP wraps the HTTP container.
type DCP struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 DCP"`
	HTTP    HTTP     `xml:"HTTP"`
}

// Value is a simple string value.
type Value struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Value"`
	Value   string   `xml:",cdata"`
}

// AllowedValues is list positive list of values.
type AllowedValues struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 AllowedValues"`
	Values  []Value  `xml:"Value"`
}

// Parameter is a named parameter with a list of allowed values.
type Parameter struct {
	XMLName       xml.Name      `xml:"http://www.opengis.net/ows/1.1 Parameter"`
	Name          string        `xml:"name,attr"`
	AllowedValues AllowedValues `xml:"AllowedValues"`
}

// DefaultValue is the default value of a constraint.
type DefaultValue struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 DefaultValue"`
	Value   string   `xml:",cdata"`
}

// Constraint is a named constraint with a list of allowed values
// and a default value.
type Constraint struct {
	XMLName       xml.Name      `xml:"http://www.opengis.net/ows/1.1 Constraint"`
	Name          string        `xml:"name,attr"`
	AllowedValues AllowedValues `xml:"AllowedValues"`
	DefaultValue  *DefaultValue `xml:"DefaultValue"`
}

// Operation contains informations of a WFS operation.
type Operation struct {
	XMLName     xml.Name      `xml:"http://www.opengis.net/ows/1.1 Operation"`
	Name        string        `xml:"name,attr"`
	DCP         DCP           `xml:"DCP"`
	Parameters  []*Parameter  `xml:"Parameter"`
	Constraints []*Constraint `xml:"Constraint"`
}

// OperationsMetadata is list of operations and constraints.
type OperationsMetadata struct {
	XMLName     xml.Name      `xml:"http://www.opengis.net/ows/1.1 OperationsMetadata"`
	Operations  []*Operation  `xml:"Operation"`
	Constraints []*Constraint `xml:"Constraint"`
}

// WGS84BoundingBox is a bounding box feature type in WGS84.
type WGS84BoundingBox struct {
	XMLName     xml.Name `xml:"http://www.opengis.net/ows/1.1 WGS84BoundingBox"`
	LowerCorner string   `xml:"LowerCorner"`
	UpperCorner string   `xml:"UpperCorner"`
}

// FeatureType is layer served by the WFS:
type FeatureType struct {
	XMLName          xml.Name          `xml:"http://www.opengis.net/wfs/2.0 FeatureType"`
	Name             string            `xml:"Name"`
	Title            string            `xml:"Title"`
	Abstract         string            `xml:"Abstract"`
	Keywords         Keywords          `xml:"Keywords"`
	DefaultCRS       string            `xml:"DefaultCRS"`
	OtherCRSs        []string          `xml:"OtherCRS"`
	WGS84BoundingBox *WGS84BoundingBox `xml:"WGS84BoundingBox"`
	Namespaces       []xml.Name        `xml:"-"`
}

// shadowFeatureType is used to prevent recursive UnmarshalXML for FeatureType.
type shadowFeatureType struct {
	XMLName          xml.Name          `xml:"http://www.opengis.net/wfs/2.0 FeatureType"`
	Name             string            `xml:"Name"`
	Title            string            `xml:"Title"`
	Abstract         string            `xml:"Abstract"`
	Keywords         Keywords          `xml:"Keywords"`
	DefaultCRS       string            `xml:"DefaultCRS"`
	OtherCRSs        []string          `xml:"OtherCRS"`
	WGS84BoundingBox *WGS84BoundingBox `xml:"WGS84BoundingBox"`
	Namespaces       []xml.Name        `xml:"-"`
}

// UnmarshalXML implements xml.Unmarshaler for better namespace handling.
func (ft *FeatureType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	// Filter out the namespaces for this feature type.
	var ns []xml.Name
	for _, attr := range start.Attr {
		if attr.Name.Space == "xmlns" {
			ns = append(ns, xml.Name{Space: attr.Name.Local, Local: attr.Value})
		}
	}
	var sft shadowFeatureType
	if err := d.DecodeElement(&sft, &start); err != nil {
		return err
	}
	*ft = FeatureType(sft)
	ft.Namespaces = ns
	return nil
}

// FeatureTypeList is the list of layers served by the WFS.
type FeatureTypeList struct {
	XMLName      xml.Name       `xml:"http://www.opengis.net/wfs/2.0 FeatureTypeList"`
	FeatureTypes []*FeatureType `xml:"FeatureType"`
}

// Capabilities is the top level metadata struct.
type Capabilities struct {
	XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 WFS_Capabilities"`

	BaseURL string `xml:"-"`

	ServiceIdentification ServiceIdentification
	OperationsMetadata    OperationsMetadata
	FeatureTypeList       FeatureTypeList
}

// FindOperation searches the capabilities for a specifc operation.
// Returns nil if not found.
func (c *Capabilities) FindOperation(name string) *Operation {
	for _, op := range c.OperationsMetadata.Operations {
		if op.Name == name {
			return op
		}
	}
	return nil
}

// SupportsHits checks if a operation supports the hits request.
func (op *Operation) SupportsHits() bool {
	for _, p := range op.Parameters {
		if p.Name == "resultType" {
			for _, av := range p.AllowedValues.Values {
				if av.Value == "hits" {
					return true
				}
			}
		}
	}
	return false
}

// SupportsOutputFormat checks if one of the given formats is supported.
func (op *Operation) SupportsOutputFormat(formats ...string) string {
	for _, p := range op.Parameters {
		if p.Name == "outputFormat" {
			for _, av := range p.AllowedValues.Values {
				v := strings.ToLower(av.Value)
				for _, f := range formats {
					if v == strings.ToLower(f) {
						return av.Value
					}
				}
			}
		}
	}
	return ""
}

// FeaturesPerPage returns the number of features per page.
// Returns if paging is not supported by the operation.
func (op *Operation) FeaturesPerPage() (int, bool) {
	for _, c := range op.Constraints {
		if c.Name == "CountDefault" {
			if c.DefaultValue != nil {
				if v, err := strconv.Atoi(c.DefaultValue.Value); err == nil {
					return v, true
				}
			}
			for _, av := range c.AllowedValues.Values {
				if v, err := strconv.Atoi(av.Value); err == nil {
					return v, true
				}

			}
		}
	}
	return 0, false
}

// FindFeatureType searches the layers for a given name.
// Returns nil if not found.
func (c *Capabilities) FindFeatureType(name string) *FeatureType {
	for _, ft := range c.FeatureTypeList.FeatureTypes {
		if ft.Name == name {
			return ft
		}
	}
	return nil
}

// FindParameter searches for named parameter. Returns nil
// if not found.
func (op *Operation) FindParameter(name string) *Parameter {
	for _, p := range op.Parameters {
		if p.Name == name {
			return p
		}
	}
	return nil
}

// WFS200 is dotted version string of version 2.0.0.
const WFS200 = "2.0.0"

var versionRe = regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`)

func versionIsLess(a, b string) bool {
	am := versionRe.FindStringSubmatch(a)
	bm := versionRe.FindStringSubmatch(b)

	var n int
	if len(am) < len(bm) {
		n = len(am)
	} else {
		n = len(bm)
	}
	n--

	for i := 0; i < n; i++ {
		ai, _ := strconv.Atoi(am[i+1])
		bi, _ := strconv.Atoi(bm[i+1])
		switch {
		case ai < bi:
			return true
		case ai > bi:
			return false
		}
	}
	return false
}

func maxVersion(a, b string) string {
	am := versionRe.FindStringSubmatch(a)
	bm := versionRe.FindStringSubmatch(b)

	var n int
	if len(am) < len(bm) {
		n = len(am)
	} else {
		n = len(bm)
	}
	n--

	for i := 0; i < n; i++ {
		ai, _ := strconv.Atoi(am[i+1])
		bi, _ := strconv.Atoi(bm[i+1])
		switch {
		case ai > bi:
			return a
		case bi > ai:
			return b
		}
	}
	return a
}

// HighestWFSVersion figures out the highest supported WFS version.
// Defaults to def.
func (c *Capabilities) HighestWFSVersion(def string) string {
	op := c.FindOperation("GetCapabilities")
	if op == nil {
		return def
	}
	p := op.FindParameter("AcceptVersions")
	if p == nil {
		return def
	}
	if len(p.AllowedValues.Values) == 0 {
		return def
	}

	max := p.AllowedValues.Values[0].Value
	for _, v := range p.AllowedValues.Values[1:] {
		max = maxVersion(max, v.Value)
	}

	return max
}

var (
	// ErrInvalidCRS is returned if a given string is not valid CRS URN.
	ErrInvalidCRS = errors.New("invalid CRS string")
	crsRe         = regexp.MustCompile(`urn:ogc:def:crs:EPSG:[^:]*:(\d+)`)
)

// CRSToEPSG extracts the EPSG code from a given CRS URN string.
func CRSToEPSG(s string) (int, error) {
	m := crsRe.FindStringSubmatch(s)
	if m == nil {
		return 0, ErrInvalidCRS
	}
	return strconv.Atoi(m[1])
}

// ParseCapabilities constructs a capabilities document from an io.Reader.
func ParseCapabilities(r io.Reader) (*Capabilities, error) {

	decoder := xml.NewDecoder(r)
	decoder.CharsetReader = charset.NewReaderLabel

	var capabilities Capabilities

	if err := decoder.Decode(&capabilities); err != nil {
		return nil, err
	}

	return &capabilities, nil
}