view pkg/geoserver/boot.go @ 876:8b9bd9ccdd93 geo-style

Upload style during boot. TODO: Connect with layer.
author Sascha L. Teichmann <teichmann@intevation.de>
date Sun, 30 Sep 2018 19:42:16 +0200
parents 371c756f0370
children 254cd247826d
line wrap: on
line source

package geoserver

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"gemma.intevation.de/gemma/pkg/config"
	"gemma.intevation.de/gemma/pkg/models"
)

const (
	workspaceName  = "gemma"
	datastoreName  = "gemma"
	databaseScheme = "waterway"
	databaseType   = "postgis"
)

const (
	startupSQL = `SELECT public.setrole('${user,'||encode('waterway_user', 'hex')||'}')`
	closeupSQL = `RESET ROLE`
)

func basicAuth(user, password string) func(req *http.Request) {
	return func(req *http.Request) {
		req.SetBasicAuth(user, password)
	}
}

func asJSON(req *http.Request) {
	req.Header.Set("Content-Type", "application/json")
}

func asContentType(req *http.Request, contentType string) {
	req.Header.Set("Content-Type", contentType)
}

func ensureWorkspace() error {
	var (
		geoURL   = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	// Probe  workspace.
	req, err := http.NewRequest(
		http.MethodGet,
		geoURL+"/rest/workspaces/"+workspaceName+".json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusNotFound {
		log.Println("info: workspace " + workspaceName + " already exists.")
		return nil
	}

	// Create workspace

	log.Println("info: creating workspace " + workspaceName)

	const createJSON = `{"workspace":{"name":"` + workspaceName + `"}}`

	req, err = http.NewRequest(
		http.MethodPost,
		geoURL+"/rest/workspaces",
		strings.NewReader(createJSON))
	if err != nil {
		return err
	}
	auth(req)
	asJSON(req)
	if resp, err = http.DefaultClient.Do(req); err != nil {
		return err
	}

	if resp.StatusCode != http.StatusCreated {
		err = fmt.Errorf("Status code '%s' (%d)",
			http.StatusText(resp.StatusCode),
			resp.StatusCode)
	}

	return err
}

func ensureDataStore() error {
	var (
		geoURL   = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	// Probe datastore.
	req, err := http.NewRequest(
		http.MethodGet,
		geoURL+"/rest/workspaces/"+workspaceName+"/datastores/"+datastoreName+".json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusNotFound {
		log.Println("info: datastore " + datastoreName + " already exists.")
		return nil
	}

	// Create datastore.
	log.Println("info: creating datastore " + datastoreName)

	type entry struct {
		Key   interface{} `json:"@key"`
		Value interface{} `json:"$"`
	}

	// Create datastore.
	ds := map[string]interface{}{
		"dataStore": map[string]interface{}{
			"name": datastoreName,
			"connectionParameters": map[string]interface{}{
				"entry": []entry{
					{"host", config.DBHost()},
					{"port", config.DBPort()},
					{"database", config.DBName()},
					{"schema", databaseScheme},
					{"user", config.DBUser()},
					{"passwd", config.DBPassword()},
					{"dbtype", databaseType},
					{"Session startup SQL", startupSQL},
					{"Session close-up SQL", closeupSQL},
				},
			},
		},
	}
	var out bytes.Buffer
	enc := json.NewEncoder(&out)
	if err := enc.Encode(&ds); err != nil {
		return err
	}

	req, err = http.NewRequest(
		http.MethodPost,
		geoURL+"/rest/workspaces/"+workspaceName+"/datastores",
		bytes.NewReader(out.Bytes()))
	if err != nil {
		return err
	}
	auth(req)
	asJSON(req)
	resp, err = http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusCreated {
		err = fmt.Errorf("Status code '%s' (%d)",
			http.StatusText(resp.StatusCode),
			resp.StatusCode)
	}

	return err
}

func ensureFeatures() error {
	var (
		geoURL   = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	tables := models.InternalServices.Filter(models.IntWFS)
	if len(tables) == 0 {
		log.Println("info: no tables to publish")
		return nil
	}

	log.Printf("info: number of tables to publish %d\n", len(tables))

	var features struct {
		FeatureTypes struct {
			FeatureType []struct {
				Name string `json:"name"`
			} `json:"featureType"`
		} `json:"featureTypes"`
	}

	hasFeature := func(name string) bool {
		for _, ft := range features.FeatureTypes.FeatureType {
			if ft.Name == name {
				return true
			}
		}
		return false
	}

	// Fetch all featuretypes.
	req, err := http.NewRequest(
		http.MethodGet,
		geoURL+"/rest/workspaces/"+workspaceName+
			"/datastores/"+datastoreName+
			"/featuretypes.json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	err = json.NewDecoder(resp.Body).Decode(&features)
	resp.Body.Close()
	if err != nil {
		// XXX: Quirk in the JSON return by GeoServer:
		// If there are no features in the datastore
		// featureType deserializes to an empty string ""
		// instead of an empty array *palmface*.
		// So assume there no features.
		hasFeature = func(string) bool { return false }
	}

	for i := range tables {
		table := tables[i].Name

		if hasFeature(table) {
			log.Printf("info: featuretype %s already exists.\n", table)
			continue
		}

		// Create featuretype.
		log.Printf("info: creating featuretype %s.\n", table)

		// Create featuretype
		ft := map[string]interface{}{
			"featureType": map[string]interface{}{
				"name":       table,
				"nativeName": table,
				"title":      table,
			},
		}

		var out bytes.Buffer
		enc := json.NewEncoder(&out)
		if err := enc.Encode(&ft); err != nil {
			return err
		}

		req, err := http.NewRequest(
			http.MethodPost,
			geoURL+"/rest/workspaces/"+workspaceName+
				"/datastores/"+datastoreName+
				"/featuretypes",
			bytes.NewReader(out.Bytes()))
		if err != nil {
			return err
		}
		auth(req)
		asJSON(req)
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}

		if resp.StatusCode != http.StatusCreated {
			return fmt.Errorf("Status code '%s' (%d)",
				http.StatusText(resp.StatusCode),
				resp.StatusCode)
		}
	}

	return nil
}

func deleteWorkspace() error {

	// Should we delete our workspace first?
	if !config.GeoServerClean() {
		return nil
	}

	log.Println("info: delete workspace " + workspaceName)
	var (
		geoURL   = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	req, err := http.NewRequest(
		http.MethodDelete,
		geoURL+"/rest/workspaces/"+workspaceName+"?recurse=true",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	_, err = http.DefaultClient.Do(req)
	return err
}

func ensureStyles() error {
	log.Println("info: creating styles")

	var (
		geoURL   = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	type styles struct {
		Styles struct {
			Style []struct {
				Name string `json:"name"`
			} `json:"style"`
		} `json:"styles"`
	}

	var stls styles

	hasStyle := func(name string) bool {
		for i := range stls.Styles.Style {
			if stls.Styles.Style[i].Name == name {
				return true
			}
		}
		return false
	}

	req, err := http.NewRequest(
		http.MethodGet,
		geoURL+"/rest/workspaces/"+workspaceName+"/styles.json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	// Fetch all styles
	if err := json.NewDecoder(resp.Body).Decode(&stls); err != nil {
		// XXX: Same quirk as with featuretypes.
	}
	resp.Body.Close()

	entries := models.InternalServices.Filter(
		models.IntAnd(models.IntWMS, models.IntWithStyle))

	for i := range entries {
		if hasStyle(entries[i].Name) {
			log.Printf("already has style for %s\n", entries[i].Name)
			continue
		}

		log.Printf("creating style %s\n", entries[i].Name)

		req, err := http.NewRequest(
			http.MethodPost,
			geoURL+"/rest/workspaces/"+workspaceName+"/styles?name="+
				url.QueryEscape(entries[i].Name),
			strings.NewReader(entries[i].Style.String))
		if err != nil {
			return err
		}
		auth(req)
		if isSymbologyEncoding(entries[i].Style.String) {
			asContentType(req, "application/vnd.ogc.se+xml")
		} else {
			asContentType(req, "application/vnd.ogc.sld+xml")
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}

		if resp.StatusCode != http.StatusCreated {
			return fmt.Errorf("cannot create style %s (%s)",
				entries[i].Name, http.StatusText(resp.StatusCode))
		}

		// TODO: Connect with feature.
	}

	return nil
}

// isSymbologyEncoding tries to figure out if its plain SLD or SE.
func isSymbologyEncoding(data string) bool {
	decoder := xml.NewDecoder(strings.NewReader(data))
	for {
		tok, err := decoder.Token()
		switch {
		case tok == nil && err == io.EOF:
			return false
		case err != nil:
			log.Printf("warn: invalid XML: %v\n", err)
			return false
		}
		if t, ok := tok.(xml.StartElement); ok &&
			t.Name.Space == "http://www.opengis.net/se" {
			return true
		}
	}
}

func prepareGeoServer() error {

	if config.DBUser() == "" {
		log.Println("info: Need metamorphic db user to configure GeoServer")
		return nil
	}

	if config.GeoServerURL() == "" {
		log.Println("info: No URL to GeoServer configured")
		return nil
	}

	for _, ensure := range []func() error{
		deleteWorkspace,
		ensureWorkspace,
		ensureDataStore,
		ensureFeatures,
		ensureStyles,
	} {
		if err := ensure(); err != nil {
			return err
		}
	}

	return nil
}

func ConfigureBoot() {
	log.Println("Configure GeoServer...")
	const maxTries = 10
	const sleep = time.Second * 5

	for try := 1; try <= maxTries; try++ {
		err := prepareGeoServer()
		if err == nil {
			break
		}
		if try < maxTries {
			if uerr, ok := err.(*url.Error); ok {
				if oerr, ok := uerr.Err.(*net.OpError); ok && oerr.Op == "dial" {
					log.Printf("Failed attempt %d of %d to configure GeoServer. "+
						"Will try again in %s...\n", try, maxTries, sleep)
					time.Sleep(sleep)
					continue
				}
			}
		}
		log.Printf("warn: configure GeoServer failed: %v\n", err)
		break
	}
}