mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-13 00:03:09 -04:00
572 lines
21 KiB
Python
572 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
***************************************************************************
|
|
catalog.py
|
|
---------------------
|
|
Date : November 2012
|
|
Copyright : (C) 2012 by Victor Olaya
|
|
Email : volayaf at gmail dot com
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************
|
|
"""
|
|
|
|
__author__ = 'Victor Olaya'
|
|
__date__ = 'November 2012'
|
|
__copyright__ = '(C) 2012, Victor Olaya'
|
|
|
|
# This will get replaced with a git SHA1 when you do a git archive
|
|
|
|
__revision__ = '$Format:%H$'
|
|
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
from os import unlink
|
|
from xml.etree.ElementTree import XML
|
|
from xml.parsers.expat import ExpatError
|
|
from urlparse import urlparse
|
|
|
|
from processing.admintools.geoserver.layer import Layer
|
|
from processing.admintools.geoserver.style import Style
|
|
from processing.admintools.geoserver.store import coveragestore_from_index, \
|
|
datastore_from_index, UnsavedDataStore, UnsavedCoverageStore
|
|
from processing.admintools.geoserver.support import prepare_upload_bundle, url
|
|
from processing.admintools.geoserver.layergroup import LayerGroup, \
|
|
UnsavedLayerGroup
|
|
from processing.admintools.geoserver.workspace import workspace_from_index, \
|
|
Workspace
|
|
from processing.admintools import httplib2
|
|
|
|
|
|
logger = logging.getLogger('gsconfig.catalog')
|
|
|
|
|
|
class UploadError(Exception):
|
|
pass
|
|
|
|
|
|
class ConflictingDataError(Exception):
|
|
pass
|
|
|
|
|
|
class AmbiguousRequestError(Exception):
|
|
pass
|
|
|
|
|
|
class FailedRequestError(Exception):
|
|
pass
|
|
|
|
|
|
def _name(named):
|
|
"""Get the name out of an object.
|
|
This varies based on the type of the input:
|
|
* the "name" of a string is itself
|
|
* the "name" of None is itself
|
|
* the "name" of an object with a property named name is that
|
|
property - as long as it's a string
|
|
* otherwise, we raise a ValueError
|
|
"""
|
|
|
|
if isinstance(named, basestring) or named is None:
|
|
return named
|
|
elif hasattr(named, 'name') and isinstance(named.name, basestring):
|
|
return named.name
|
|
else:
|
|
raise ValueError(
|
|
"Can't interpret %s as a name or a configuration object"
|
|
% named)
|
|
|
|
|
|
class Catalog(object):
|
|
"""The GeoServer catalog represents all of the information in the
|
|
GeoServer configuration. This includes:
|
|
- Stores of geospatial data
|
|
- Resources, or individual coherent datasets within stores
|
|
- Styles for resources
|
|
- Layers, which combine styles with resources to create a visible map
|
|
layer
|
|
- LayerGroups, which alias one or more layers for convenience
|
|
- Workspaces, which provide logical grouping of Stores
|
|
- Maps, which provide a set of OWS services with a subset of the server's
|
|
Layers
|
|
- Namespaces, which provide unique identifiers for resources
|
|
"""
|
|
|
|
def __init__(self, service_url, username='admin', password='geoserver',
|
|
disable_ssl_certificate_validation=False):
|
|
self.service_url = service_url
|
|
if self.service_url.endswith('/'):
|
|
self.service_url = self.service_url.strip('/')
|
|
self.http = httplib2.Http(
|
|
disable_ssl_certificate_validation=disable_ssl_certificate_validation)
|
|
self.username = username
|
|
self.password = password
|
|
self.http.add_credentials(self.username, self.password)
|
|
netloc = urlparse(service_url).netloc
|
|
self.http.authorizations.append(httplib2.BasicAuthentication(
|
|
(username, password),
|
|
netloc,
|
|
service_url,
|
|
{},
|
|
None,
|
|
None,
|
|
self.http,
|
|
))
|
|
self._cache = dict()
|
|
|
|
def delete(self, config_object, purge=False, recurse=False):
|
|
"""Send a delete request
|
|
"""
|
|
|
|
rest_url = config_object.href
|
|
|
|
# params aren't supported fully in httplib2 yet, so:
|
|
params = []
|
|
|
|
# purge deletes the SLD from disk when a style is deleted
|
|
if purge:
|
|
params.append('purge=true')
|
|
|
|
# recurse deletes the resource when a layer is deleted.
|
|
if recurse:
|
|
params.append('recurse=true')
|
|
|
|
if params:
|
|
rest_url = rest_url + '?' + '&'.join(params)
|
|
|
|
headers = {'Content-type': 'application/xml',
|
|
'Accept': 'application/xml'}
|
|
(response, content) = self.http.request(rest_url, 'DELETE',
|
|
headers=headers)
|
|
self._cache.clear()
|
|
|
|
if response.status == 200:
|
|
return (response, content)
|
|
else:
|
|
raise FailedRequestError(
|
|
'Tried to make a DELETE request to %s but got a %d \
|
|
status code: \n%s'
|
|
% (rest_url, response.status, content))
|
|
|
|
def get_xml(self, rest_url):
|
|
logger.debug('GET %s', rest_url)
|
|
|
|
cached_response = self._cache.get(rest_url)
|
|
|
|
def is_valid(cached_response):
|
|
return cached_response is not None and datetime.now() \
|
|
- cached_response[0] < timedelta(seconds=5)
|
|
|
|
def parse_or_raise(xml):
|
|
try:
|
|
return XML(xml)
|
|
except (ExpatError, SyntaxError), e:
|
|
msg = 'GeoServer gave non-XML response for [GET %s]: %s'
|
|
msg = msg % (rest_url, xml)
|
|
raise Exception(msg, e)
|
|
|
|
if is_valid(cached_response):
|
|
raw_text = cached_response[1]
|
|
return parse_or_raise(raw_text)
|
|
else:
|
|
(response, content) = self.http.request(rest_url)
|
|
if response.status == 200:
|
|
self._cache[rest_url] = (datetime.now(), content)
|
|
return parse_or_raise(content)
|
|
else:
|
|
raise FailedRequestError(
|
|
'Tried to make a GET request to %s but got a %d \
|
|
status code: \n%s'
|
|
% (url, response.status, content))
|
|
|
|
def reload(self):
|
|
reload_url = url(self.service_url, ['reload'])
|
|
response = self.http.request(reload_url, 'POST')
|
|
self._cache.clear()
|
|
return response
|
|
|
|
def save(self, obj):
|
|
"""Saves an object to the REST service.
|
|
|
|
Gets the object's REST location and the XML from the object,
|
|
then POSTS the request.
|
|
"""
|
|
|
|
rest_url = obj.href
|
|
message = obj.message()
|
|
|
|
headers = {'Content-type': 'application/xml',
|
|
'Accept': 'application/xml'}
|
|
logger.debug('%s %s', obj.save_method, obj.href)
|
|
response = self.http.request(rest_url, obj.save_method, message,
|
|
headers)
|
|
(headers, body) = response
|
|
self._cache.clear()
|
|
if 400 <= int(headers['status']) < 600:
|
|
raise FailedRequestError('Error code (%s) from GeoServer: %s'
|
|
% (headers['status'], body))
|
|
return response
|
|
|
|
def get_store(self, name, workspace=None):
|
|
if workspace is None:
|
|
store = None
|
|
for ws in self.get_workspaces():
|
|
found = None
|
|
try:
|
|
found = self.get_store(name, ws)
|
|
except:
|
|
# Don't expect every workspace to contain the named store
|
|
pass
|
|
if found:
|
|
if store:
|
|
raise AmbiguousRequestError(
|
|
'Multiple stores found named: ' + name)
|
|
else:
|
|
store = found
|
|
if not store:
|
|
raise FailedRequestError('No store found named: ' + name)
|
|
return store
|
|
else:
|
|
# Workspace is not None
|
|
if isinstance(workspace, basestring):
|
|
workspace = self.get_workspace(workspace)
|
|
if workspace is None:
|
|
return None
|
|
logger.debug('datastore url is [%s]', workspace.datastore_url)
|
|
ds_list = self.get_xml(workspace.datastore_url)
|
|
cs_list = self.get_xml(workspace.coveragestore_url)
|
|
datastores = [n for n in ds_list.findall('dataStore')
|
|
if n.find('name').text == name]
|
|
coveragestores = [n for n in cs_list.findall('coverageStore')
|
|
if n.find('name').text == name]
|
|
(ds_len, cs_len) = (len(datastores), len(coveragestores))
|
|
|
|
if ds_len == 1 and cs_len == 0:
|
|
return datastore_from_index(self, workspace, datastores[0])
|
|
elif ds_len == 0 and cs_len == 1:
|
|
return coveragestore_from_index(self, workspace,
|
|
coveragestores[0])
|
|
elif ds_len == 0 and cs_len == 0:
|
|
raise FailedRequestError('No store found in ' + str(workspace)
|
|
+ ' named: ' + name)
|
|
else:
|
|
raise AmbiguousRequestError(str(workspace) + ' and name: '
|
|
+ name + ' do not uniquely identify a layer')
|
|
|
|
def get_stores(self, workspace=None):
|
|
if workspace is not None:
|
|
if isinstance(workspace, basestring):
|
|
workspace = self.get_workspace(workspace)
|
|
ds_list = self.get_xml(workspace.datastore_url)
|
|
cs_list = self.get_xml(workspace.coveragestore_url)
|
|
datastores = [datastore_from_index(self, workspace, n) for n in
|
|
ds_list.findall('dataStore')]
|
|
coveragestores = [coveragestore_from_index(self, workspace, n)
|
|
for n in cs_list.findall('coverageStore')]
|
|
return datastores + coveragestores
|
|
else:
|
|
stores = []
|
|
for ws in self.get_workspaces():
|
|
a = self.get_stores(ws)
|
|
stores.extend(a)
|
|
return stores
|
|
|
|
def create_datastore(self, name, workspace=None):
|
|
if isinstance(workspace, basestring):
|
|
workspace = self.get_workspace(workspace)
|
|
elif workspace is None:
|
|
workspace = self.get_default_workspace()
|
|
return UnsavedDataStore(self, name, workspace)
|
|
|
|
def create_coveragestore2(self, name, workspace=None):
|
|
"""Hm we already named the method that creates a coverage *resource*
|
|
create_coveragestore... time for an API break?
|
|
"""
|
|
|
|
if isinstance(workspace, basestring):
|
|
workspace = self.get_workspace(workspace)
|
|
elif workspace is None:
|
|
workspace = self.get_default_workspace()
|
|
return UnsavedCoverageStore(self, name, workspace)
|
|
|
|
def add_data_to_store(self, store, name, data, workspace=None,
|
|
overwrite=False, charset=None):
|
|
if isinstance(store, basestring):
|
|
store = self.get_store(store, workspace=workspace)
|
|
if workspace is not None:
|
|
workspace = _name(workspace)
|
|
assert store.workspace.name == workspace, \
|
|
'Specified store (%s) is not in specified workspace (%s)!' \
|
|
% (store, workspace)
|
|
else:
|
|
workspace = store.workspace.name
|
|
store = store.name
|
|
|
|
if isinstance(data, dict):
|
|
bundle = prepare_upload_bundle(name, data)
|
|
else:
|
|
bundle = data
|
|
|
|
params = dict()
|
|
if overwrite:
|
|
params['update'] = 'overwrite'
|
|
if charset is not None:
|
|
params['charset'] = charset
|
|
|
|
message = open(bundle)
|
|
headers = {'Content-Type': 'application/zip',
|
|
'Accept': 'application/xml'}
|
|
upload_url = url(self.service_url, ['workspaces', workspace,
|
|
'datastores', store, 'file.shp'], params)
|
|
|
|
try:
|
|
(headers, response) = self.http.request(upload_url, 'PUT',
|
|
message, headers)
|
|
self._cache.clear()
|
|
if headers.status != 201:
|
|
raise UploadError(response)
|
|
finally:
|
|
unlink(bundle)
|
|
|
|
def create_featurestore(self, name, data, workspace=None, overwrite=False,
|
|
charset=None):
|
|
if not overwrite:
|
|
try:
|
|
store = self.get_store(name, workspace)
|
|
msg = 'There is already a store named ' + name
|
|
if workspace:
|
|
msg += ' in ' + str(workspace)
|
|
raise ConflictingDataError(msg)
|
|
except FailedRequestError:
|
|
# We don't really expect that every layer name will be taken
|
|
pass
|
|
|
|
if workspace is None:
|
|
workspace = self.get_default_workspace()
|
|
workspace = _name(workspace)
|
|
params = dict()
|
|
if charset is not None:
|
|
params['charset'] = charset
|
|
ds_url = url(self.service_url, ['workspaces', workspace, 'datastores',
|
|
name, 'file.shp'], params)
|
|
|
|
# PUT /workspaces/<ws>/datastores/<ds>/file.shp
|
|
headers = {'Content-type': 'application/zip',
|
|
'Accept': 'application/xml'}
|
|
if isinstance(data, dict):
|
|
logger.debug('Data is NOT a zipfile')
|
|
archive = prepare_upload_bundle(name, data)
|
|
else:
|
|
logger.debug('Data is a zipfile')
|
|
archive = data
|
|
message = open(archive)
|
|
try:
|
|
(headers, response) = self.http.request(ds_url, 'PUT', message,
|
|
headers)
|
|
self._cache.clear()
|
|
if headers.status != 201:
|
|
raise UploadError(response)
|
|
finally:
|
|
unlink(archive)
|
|
|
|
def create_coveragestore(self, name, data, workspace=None,
|
|
overwrite=False):
|
|
if not overwrite:
|
|
try:
|
|
store = self.get_store(name, workspace)
|
|
msg = 'There is already a store named ' + name
|
|
if workspace:
|
|
msg += ' in ' + str(workspace)
|
|
raise ConflictingDataError(msg)
|
|
except FailedRequestError:
|
|
# We don't really expect that every layer name will be taken
|
|
pass
|
|
|
|
if workspace is None:
|
|
workspace = self.get_default_workspace()
|
|
headers = {'Content-type': 'image/tiff', 'Accept': 'application/xml'}
|
|
|
|
archive = None
|
|
ext = 'geotiff'
|
|
|
|
if isinstance(data, dict):
|
|
archive = prepare_upload_bundle(name, data)
|
|
message = open(archive)
|
|
if 'tfw' in data:
|
|
headers['Content-type'] = 'application/archive'
|
|
ext = 'worldimage'
|
|
elif isinstance(data, basestring):
|
|
message = open(data)
|
|
else:
|
|
message = data
|
|
|
|
cs_url = url(self.service_url, ['workspaces', workspace.name,
|
|
'coveragestores', name, 'file.' + ext])
|
|
|
|
try:
|
|
(headers, response) = self.http.request(cs_url, 'PUT', message,
|
|
headers)
|
|
self._cache.clear()
|
|
if headers.status != 201:
|
|
raise UploadError(response)
|
|
finally:
|
|
if archive is not None:
|
|
unlink(archive)
|
|
|
|
def get_resource(self, name, store=None, workspace=None):
|
|
if store is not None:
|
|
candidates = [s for s in self.get_resources(store) if s.name
|
|
== name]
|
|
if len(candidates) == 0:
|
|
return None
|
|
elif len(candidates) > 1:
|
|
raise AmbiguousRequestError
|
|
else:
|
|
return candidates[0]
|
|
|
|
if workspace is not None:
|
|
for store in self.get_stores(workspace):
|
|
resource = self.get_resource(name, store)
|
|
if resource is not None:
|
|
return resource
|
|
return None
|
|
|
|
for ws in self.get_workspaces():
|
|
resource = self.get_resource(name, workspace=ws)
|
|
if resource is not None:
|
|
return resource
|
|
return None
|
|
|
|
def get_resources(self, store=None, workspace=None):
|
|
if isinstance(workspace, basestring):
|
|
workspace = self.get_workspace(workspace)
|
|
if isinstance(store, basestring):
|
|
store = self.get_store(store, workspace)
|
|
if store is not None:
|
|
return store.get_resources()
|
|
if workspace is not None:
|
|
resources = []
|
|
for store in self.get_stores(workspace):
|
|
resources.extend(self.get_resources(store))
|
|
return resources
|
|
resources = []
|
|
for ws in self.get_workspaces():
|
|
resources.extend(self.get_resources(workspace=ws))
|
|
return resources
|
|
|
|
def get_layer(self, name):
|
|
try:
|
|
lyr = Layer(self, name)
|
|
lyr.fetch()
|
|
return lyr
|
|
except FailedRequestError:
|
|
return None
|
|
|
|
def get_layers(self, resource=None):
|
|
if isinstance(resource, basestring):
|
|
resource = self.get_resource(resource)
|
|
layers_url = url(self.service_url, ['layers.xml'])
|
|
description = self.get_xml(layers_url)
|
|
lyrs = [Layer(self, l.find('name').text) for l in
|
|
description.findall('layer')]
|
|
if resource is not None:
|
|
lyrs = [l for l in lyrs if l.resource.href == resource.href]
|
|
|
|
# TODO: filter by style
|
|
return lyrs
|
|
|
|
def get_layergroup(self, name=None):
|
|
try:
|
|
group_url = url(self.service_url, ['layergroups', name + '.xml'])
|
|
group = self.get_xml(group_url)
|
|
return LayerGroup(self, group.find('name').text)
|
|
except FailedRequestError:
|
|
return None
|
|
|
|
def get_layergroups(self):
|
|
groups = self.get_xml('%s/layergroups.xml' % self.service_url)
|
|
return [LayerGroup(self, g.find('name').text) for g in
|
|
groups.findall('layerGroup')]
|
|
|
|
def create_layergroup(self, name, layers=(), styles=(), bounds=None):
|
|
if any(g.name == name for g in self.get_layergroups()):
|
|
raise ConflictingDataError('LayerGroup named %s already exists!'
|
|
% name)
|
|
else:
|
|
return UnsavedLayerGroup(self, name, layers, styles, bounds)
|
|
|
|
def get_style(self, name):
|
|
try:
|
|
style_url = url(self.service_url, ['styles', name + '.xml'])
|
|
dom = self.get_xml(style_url)
|
|
return Style(self, dom.find('name').text)
|
|
except FailedRequestError:
|
|
return None
|
|
|
|
def get_styles(self):
|
|
styles_url = url(self.service_url, ['styles.xml'])
|
|
description = self.get_xml(styles_url)
|
|
return [Style(self, s.find('name').text) for s in
|
|
description.findall('style')]
|
|
|
|
def create_style(self, name, data, overwrite=False):
|
|
if not overwrite and self.get_style(name) is not None:
|
|
raise ConflictingDataError('There is already a style named %s'
|
|
% name)
|
|
|
|
headers = {'Content-type': 'application/vnd.ogc.sld+xml',
|
|
'Accept': 'application/xml'}
|
|
|
|
if overwrite:
|
|
style_url = url(self.service_url, ['styles', name + '.sld'])
|
|
(headers, response) = self.http.request(style_url, 'PUT', data,
|
|
headers)
|
|
else:
|
|
style_url = url(self.service_url, ['styles'], dict(name=name))
|
|
(headers, response) = self.http.request(style_url, 'POST', data,
|
|
headers)
|
|
|
|
self._cache.clear()
|
|
if headers.status < 200 or headers.status > 299:
|
|
raise UploadError(response)
|
|
|
|
def create_workspace(self, name, uri):
|
|
xml = '<namespace><prefix>{name}</prefix><uri>{uri}</uri>\
|
|
</namespace>'.format(name=name, uri=uri)
|
|
headers = {'Content-Type': 'application/xml'}
|
|
workspace_url = self.service_url + '/namespaces/'
|
|
|
|
(headers, response) = self.http.request(workspace_url, 'POST', xml,
|
|
headers)
|
|
assert 200 <= headers.status < 300, \
|
|
'Tried to create workspace but got ' + str(headers.status) + ': ' \
|
|
+ response
|
|
self._cache.clear()
|
|
return self.get_workspace(name)
|
|
|
|
def get_workspaces(self):
|
|
description = self.get_xml('%s/workspaces.xml' % self.service_url)
|
|
return [workspace_from_index(self, node) for node in
|
|
description.findall('workspace')]
|
|
|
|
def get_workspace(self, name):
|
|
candidates = [w for w in self.get_workspaces() if w.name == name]
|
|
if len(candidates) == 0:
|
|
return None
|
|
elif len(candidates) > 1:
|
|
raise AmbiguousRequestError()
|
|
else:
|
|
return candidates[0]
|
|
|
|
def get_default_workspace(self):
|
|
return Workspace(self, 'default')
|
|
|
|
def set_default_workspace(self):
|
|
raise NotImplementedError()
|