view pkg/wfs/capabilities.go @ 1608:427f9010b4a9

WFS download: Started with GET downloader (paged and unpaged).
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 17 Dec 2018 18:27:57 +0100
parents e80e35b26f17
children efc409e330a6
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"
	"io"
	"regexp"
	"strconv"

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

type Keyword struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Keyword"`
	Value   string   `xml:",cdata"`
}
type Keywords struct {
	XMLName  xml.Name  `xml:"http://www.opengis.net/ows/1.1 Keywords"`
	Keywords []Keyword `xml:"Keyword"`
}

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
}

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"`
}

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"`
}

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

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

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

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

type Parameter struct {
	XMLName       xml.Name      `xml:"http://www.opengis.net/ows/1.1 Parameter"`
	Name          string        `xml:"name,attr"`
	AllowedValues AllowedValues `xml:"AllowedValues"`
}

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

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"`
}

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"`
}

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

type WGS84BoundingBox struct {
	XMLName     xml.Name `xml:"http://www.opengis.net/ows/1.1 WGS84BoundingBox"`
	LowerCorner string   `xml:"LowerCorner"`
	UpperCorner string   `xml:"UpperCorner"`
}

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:"-"`
}

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
}

type FeatureTypeList struct {
	XMLName      xml.Name       `xml:"http://www.opengis.net/wfs/2.0 FeatureTypeList"`
	FeatureTypes []*FeatureType `xml:"FeatureType"`
}

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
}

func (c *Capabilities) FindOperation(name string) *Operation {
	for _, op := range c.OperationsMetadata.Operations {
		if op.Name == name {
			return op
		}
	}
	return nil
}

func (o *Operation) SupportsHits() bool {
	for _, p := range o.Parameters {
		if p.Name == "resultType" {
			for _, av := range p.AllowedValues.Values {
				if av.Value == "hits" {
					return true
				}
			}
		}
	}
	return false
}

func (o *Operation) FeaturesPerPage() (int, bool) {
	for _, c := range o.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
}

func (c *Capabilities) FindFeatureType(name string) *FeatureType {
	for _, ft := range c.FeatureTypeList.FeatureTypes {
		if ft.Name == name {
			return ft
		}
	}
	return nil
}

func (op *Operation) FindParameter(name string) *Parameter {
	for _, p := range op.Parameters {
		if p.Name == name {
			return p
		}
	}
	return nil
}

const WFS2_0_0 = "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
}

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
}

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
}