From 601f56bb2c2379359c53f4e16d47d465efd64c67 Mon Sep 17 00:00:00 2001 From: Mykola Morozov Date: Thu, 15 Dec 2022 23:30:45 +0900 Subject: [PATCH] Fixed #5. Finished datastore service. --- .../Models/Catalog/ConnectionParameter.cs | 20 ++ .../Models/Catalog/ConnectionParameters.cs | 17 ++ .../Models/CatalogResponses/NamedLink.cs | 2 +- .../Models/Datastore/DataStoreInfo.cs | 45 +++ .../Models/Datastore/DataStoreSummary.cs | 59 ++++ .../Sources/ShapefileConnectionParameters.cs | 23 ++ .../Models/Workspace/WorkspaceInfo.cs | 2 +- .../Workspace/WorkspaceResponseWrapper.cs | 5 +- .../Models/Workspace/WorkspaceSummary.cs | 20 +- .../Services/IDatastoreService.cs | 84 +++++- .../Services/IWorkspaceService.cs | 3 + .../Extensions/GeoServerExtensions.cs | 3 +- .../ConnectionParametersConverter.cs | 127 ++++++++ .../Models/Datastore/DataStoreInfoWrapper.cs | 19 ++ .../Models/Datastore}/DataStoreListWrapper.cs | 7 +- .../Datastore/DataStoreSummaryWrapper.cs | 19 ++ .../Datastore}/DataStoresListResponse.cs | 4 +- .../Models/Workspace/GetWorkspaceResponse.cs | 2 +- .../Models/Workspace/WorkspaceWrapper.cs | 2 +- .../Models/Workspace/WorkspacesResponse.cs | 2 +- .../Resources/Messages.Designer.cs | 9 + .../Resources/Messages.resx | 3 + .../Services/DatastoreService.cs | 283 +++++++++++++++++- .../Services/WorkspaceService.cs | 2 + .../Services/DatastoreServiceTests.cs | 175 +++++++++++ .../Services/WorkspaceServiceTests.cs | 8 +- tests/GeoTools.GeoServer.Tests/Usings.cs | 1 - 27 files changed, 911 insertions(+), 35 deletions(-) create mode 100644 src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameter.cs create mode 100644 src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameters.cs create mode 100644 src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreInfo.cs create mode 100644 src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreSummary.cs create mode 100644 src/GeoTools.GeoServer.Abstractions/Models/Datastore/Sources/ShapefileConnectionParameters.cs create mode 100644 src/GeoTools.GeoServer/Helpers/Converters/ConnectionParametersConverter.cs create mode 100644 src/GeoTools.GeoServer/Models/Datastore/DataStoreInfoWrapper.cs rename src/{GeoTools.GeoServer.Abstractions/Models/Datastores => GeoTools.GeoServer/Models/Datastore}/DataStoreListWrapper.cs (69%) create mode 100644 src/GeoTools.GeoServer/Models/Datastore/DataStoreSummaryWrapper.cs rename src/{GeoTools.GeoServer.Abstractions/Models/Datastores => GeoTools.GeoServer/Models/Datastore}/DataStoresListResponse.cs (80%) create mode 100644 tests/GeoTools.GeoServer.Tests/Services/DatastoreServiceTests.cs diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameter.cs b/src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameter.cs new file mode 100644 index 0000000..dc96348 --- /dev/null +++ b/src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameter.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Models.Catalog +{ + public class ConnectionParameter + { + [JsonPropertyName("@key")] + public string Key { get; } + + [JsonPropertyName("$")] + public string Value { get; } + + [JsonConstructor] + public ConnectionParameter(string key, string value) + { + Key = key; + Value = value; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameters.cs b/src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameters.cs new file mode 100644 index 0000000..392f674 --- /dev/null +++ b/src/GeoTools.GeoServer.Abstractions/Models/Catalog/ConnectionParameters.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Models.Catalog +{ + public class ConnectionParameters + { + [JsonPropertyName("entry")] + public IList ConnectionParameterList { get; } + + [JsonConstructor] + public ConnectionParameters(IList connectionParameters) + { + ConnectionParameterList = connectionParameters; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/CatalogResponses/NamedLink.cs b/src/GeoTools.GeoServer.Abstractions/Models/CatalogResponses/NamedLink.cs index 40373ff..54575f0 100644 --- a/src/GeoTools.GeoServer.Abstractions/Models/CatalogResponses/NamedLink.cs +++ b/src/GeoTools.GeoServer.Abstractions/Models/CatalogResponses/NamedLink.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.CatalogResponses { public class NamedLink { diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreInfo.cs b/src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreInfo.cs new file mode 100644 index 0000000..b90655d --- /dev/null +++ b/src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreInfo.cs @@ -0,0 +1,45 @@ +using GeoTools.GeoServer.Models.Catalog; +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Models.Datastore +{ + /// + /// Datastore. + /// + public class DataStoreInfo + { + /// + /// Name of data store. + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Description of data store. + /// + [JsonPropertyName("description")] + public string Description { get; } + + /// + /// Whether or not the data store is enabled. + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; } = true; + + [JsonPropertyName("connectionParameters")] + public ConnectionParameters ConnectionParameters { get; } + + [JsonPropertyName("disableOnConnFailure")] + public bool DisableOnConnFailure { get; } + + [JsonConstructor] + public DataStoreInfo(string name, string description, bool enabled, ConnectionParameters connectionParameters, bool disableOnConnFailure) + { + Name = name; + Description = description; + Enabled = enabled; + ConnectionParameters = connectionParameters; + DisableOnConnFailure = disableOnConnFailure; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreSummary.cs b/src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreSummary.cs new file mode 100644 index 0000000..9871b3e --- /dev/null +++ b/src/GeoTools.GeoServer.Abstractions/Models/Datastore/DataStoreSummary.cs @@ -0,0 +1,59 @@ +using GeoTools.GeoServer.Models.Catalog; +using GeoTools.GeoServer.Models.CatalogResponses; +using System; +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Models.Datastore +{ + /// + /// Datastore. + /// + public class DataStoreSummary + { + /// + /// Name of data store. + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Description of data store. + /// + [JsonPropertyName("description")] + public string Description { get; } + + /// + /// Whether or not the data store is enabled. + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; } = true; + + [JsonPropertyName("connectionParameters")] + public ConnectionParameters ConnectionParameters { get; } + + [JsonPropertyName("workspace")] + public NamedLink Workspace { get; } + + [JsonPropertyName("_default")] + public bool Default { get; } + + [JsonPropertyName("disableOnConnFailure")] + public bool DisableOnConnFailure { get; } + + [JsonPropertyName("featureTypes")] + public Uri FeatureTypes { get; } + + [JsonConstructor] + public DataStoreSummary(string name, string description, bool enabled, ConnectionParameters connectionParameters, NamedLink workspace, bool @default, bool disableOnConnFailure, Uri featureTypes) + { + Name = name; + Description = description; + Enabled = enabled; + ConnectionParameters = connectionParameters; + Workspace = workspace; + Default = @default; + DisableOnConnFailure = disableOnConnFailure; + FeatureTypes = featureTypes; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Datastore/Sources/ShapefileConnectionParameters.cs b/src/GeoTools.GeoServer.Abstractions/Models/Datastore/Sources/ShapefileConnectionParameters.cs new file mode 100644 index 0000000..5773220 --- /dev/null +++ b/src/GeoTools.GeoServer.Abstractions/Models/Datastore/Sources/ShapefileConnectionParameters.cs @@ -0,0 +1,23 @@ +using GeoTools.GeoServer.Models.Catalog; +using System; +using System.Collections.Generic; + +namespace GeoTools.GeoServer.Models.Datastore.Sources +{ + public class ShapefileConnectionParameters : ConnectionParameters + { + public ShapefileConnectionParameters(Uri shapefilePath, Uri @namespace) : base(CreateConnectionParameters(shapefilePath, @namespace)) + { + + } + + private static IList CreateConnectionParameters(Uri shapefilePath, Uri @namespace) + { + return new List + { + new ConnectionParameter("namespace", @namespace.ToString()), + new ConnectionParameter("url", shapefilePath.ToString()), + }; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs index dcceea1..e314a75 100644 --- a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs +++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Workspace { /// /// Workspace. diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceResponseWrapper.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceResponseWrapper.cs index e2e0231..7b6a0b1 100644 --- a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceResponseWrapper.cs +++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceResponseWrapper.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using GeoTools.GeoServer.Models.CatalogResponses; +using System.Collections.Generic; using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Workspace { public class WorkspaceResponseWrapper { diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs index 467034f..4a03c28 100644 --- a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs +++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs @@ -1,6 +1,7 @@ -using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Workspace { /// /// Workspace Response. @@ -29,28 +30,35 @@ public class WorkspaceSummary /// URL to Datas tores in this workspace. /// [JsonPropertyName("dataStores")] - public string DataStores { get; } + public Uri DataStores { get; } /// /// URL to Coverage stores in this workspace. /// [JsonPropertyName("coverageStores")] - public string CoverageStores { get; } + public Uri CoverageStores { get; } /// /// URL to WMS stores in this workspace. /// [JsonPropertyName("wmsStores")] - public string WmsStores { get; } + public Uri WmsStores { get; } + + /// + /// URL to WMTS stores in this workspace. + /// + [JsonPropertyName("wmtsStores")] + public Uri WmtsStores { get; } [JsonConstructor] - public WorkspaceSummary(string name, string dataStores, string coverageStores, string wmsStores, bool isolated = false) + public WorkspaceSummary(string name, Uri dataStores, Uri coverageStores, Uri wmsStores, Uri wmtsStores, bool isolated = false) { Name = name; Isolated = isolated; DataStores = dataStores; CoverageStores = coverageStores; WmsStores = wmsStores; + WmtsStores = wmtsStores; } } } diff --git a/src/GeoTools.GeoServer.Abstractions/Services/IDatastoreService.cs b/src/GeoTools.GeoServer.Abstractions/Services/IDatastoreService.cs index ad6eb79..f9725f1 100644 --- a/src/GeoTools.GeoServer.Abstractions/Services/IDatastoreService.cs +++ b/src/GeoTools.GeoServer.Abstractions/Services/IDatastoreService.cs @@ -1,14 +1,15 @@ using GeoTools.GeoServer.Models; +using GeoTools.GeoServer.Models.CatalogResponses; +using GeoTools.GeoServer.Models.Datastore; using System; using System.Collections.Generic; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; namespace GeoTools.GeoServer.Services { public interface IDatastoreService { - /// /// Get a list of data stores. /// @@ -22,5 +23,84 @@ public interface IDatastoreService /// Def: Returns null / . /// Task>> GetDatastoresAsync(string workspaceName, CancellationToken token); + + /// + /// Retrieve a particular data store from a workspace. + /// + /// The name of the worskpace containing the data store. + /// The name of the data store to retrieve. + /// + /// Controls a particular data store in a given workspace. + /// + /// 200: OK. Returns the model. + /// 401: Missing auth configuration. Returns null / . + /// 404: Workspace or datastore does not exist. Returns null. + /// Def: Returns null / . + /// + Task> GetDatastoreAsync(string workspaceName, string datastoreName, CancellationToken token); + + /// + /// Create a new data store. + /// + /// The name of the worskpace containing the data stores. + /// + /// The data store body information to upload. + /// The contents of the connection parameters will differ depending on the type of data store being added. + /// + /// + /// Adds a new data store to the workspace. + /// + /// 201: Created. Returns true if created successfully, false if already exists. + /// 401: Missing auth configuration. Returns null / . + /// 500: Workspace not found. Returns null. + /// Def: Returns null / . + /// See examples at https://github.com/geoserver/geoserver/blob/main/src/community/rest-openapi/openapi/src/main/resources/org/geoserver/rest/openapi/1.0.0/datastores.yaml#L61. + /// + Task> CreateDatastoreAsync(string workspaceName, DataStoreInfo datastoreInfo, CancellationToken token); + + /// + /// Delete data store. + /// + /// The name of the worskpace containing the data store. + /// The name of the data store to delete. + /// + /// The recurse controls recursive deletion. When set to true all + /// resources contained in the store are also removed.The default value + /// is "false". + /// + /// + /// Deletes a data store from the server. + /// + /// 200: Success datastore deleted. Returns true. + /// 401: Missing auth configuration. Returns false / . + /// 403: Datastore is not empty (and recurse not true). Returns false. + /// 404: Workspace or datastore doesn't exist. Returns false. + /// Def: Returns false / . + /// + Task> DeleteDatastoreAsync(string workspaceName, string datastoreName, bool? recurse, CancellationToken token); + + /// + /// Modify a data store. + /// + /// The name of the worskpace containing the data store. + /// + /// The updated data store definition. + /// For a PUT, only values which should be changed need to be included. + /// The connectionParameters map counts as a single value, + /// so if you change it all preexisting connection parameters will be + /// overwritten. + /// The contents of the connection parameters will differ depending on the + /// type of data store being added. + /// + /// + /// Modify data store ds. + /// + /// 200: Modified. Returns true. + /// 401: Missing auth configuration. Returns false / . + /// 404: Workspace not found. Returns false. + /// 405: Forbidden to change the name of the datastore. Returns false. + /// Def: Returns false / . + /// + Task> UpdateDatastoreAsync(string workspaceName, DataStoreInfo datastore, CancellationToken token); } } diff --git a/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs b/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs index e248c59..ece4d3d 100644 --- a/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs +++ b/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs @@ -1,4 +1,6 @@ using GeoTools.GeoServer.Models; +using GeoTools.GeoServer.Models.CatalogResponses; +using GeoTools.GeoServer.Models.Workspace; using System; using System.Collections.Generic; using System.Threading; @@ -57,6 +59,7 @@ public interface IWorkspaceService /// Delete a Workspace. /// /// The name of the workspace to delete. + /// Delete workspace contents (default false). /// /// Deletes a single workspace definition. /// diff --git a/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs b/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs index 4f8020d..0b20460 100644 --- a/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs +++ b/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs @@ -32,7 +32,8 @@ public static IServiceCollection AddGeoServer(this IServiceCollection services, .AddHttpClient(GeoServerOptions.HttpClientName, ConfigureHttpClient); return services - .AddTransient(); + .AddTransient() + .AddTransient(); } private static void ConfigureHttpClient(IServiceProvider provider, HttpClient client) diff --git a/src/GeoTools.GeoServer/Helpers/Converters/ConnectionParametersConverter.cs b/src/GeoTools.GeoServer/Helpers/Converters/ConnectionParametersConverter.cs new file mode 100644 index 0000000..a70fbe5 --- /dev/null +++ b/src/GeoTools.GeoServer/Helpers/Converters/ConnectionParametersConverter.cs @@ -0,0 +1,127 @@ +using GeoTools.GeoServer.Models.Catalog; +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Helpers.Converters +{ + internal class ConnectionParametersConverter : JsonConverter + { + public override ConnectionParameters Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string propertyName = reader.GetString(); + if (propertyName != "entry") + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + var parameters = new List(); + + reader.Read(); + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + string key = ""; + string value = ""; + + for (int i = 0; i < 2; ++i) + { + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + var readerKey = reader.GetString(); + if (readerKey == "@key") + { + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + key = reader.GetString(); + } + else if (readerKey == "$") + { + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + value = reader.GetString(); + } + else + { + throw new JsonException($"Unknown JSON key {readerKey}."); + } + } + + parameters.Add(new ConnectionParameter(key, value)); + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException(); + } + + reader.Read(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException(); + } + + return new ConnectionParameters(parameters); + } + + public override void Write(Utf8JsonWriter writer, ConnectionParameters value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WritePropertyName("entry"); + writer.WriteStartArray(); + if (value != null && value.ConnectionParameterList != null) + { + foreach (var parameter in value.ConnectionParameterList) + { + writer.WriteStartObject(); + + writer.WriteString("@key", parameter.Key); + writer.WriteString("$", parameter.Value); + + writer.WriteEndObject(); + } + } + writer.WriteEndArray(); + + writer.WriteEndObject(); + } + } +} diff --git a/src/GeoTools.GeoServer/Models/Datastore/DataStoreInfoWrapper.cs b/src/GeoTools.GeoServer/Models/Datastore/DataStoreInfoWrapper.cs new file mode 100644 index 0000000..1b6a186 --- /dev/null +++ b/src/GeoTools.GeoServer/Models/Datastore/DataStoreInfoWrapper.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Models.Datastore +{ + /// + /// Wrapper object for DataStoreInfo, in order to comply with current API encoding. + /// + internal class DataStoreInfoWrapper + { + [JsonPropertyName("dataStore")] + public DataStoreInfo DataStore { get; } + + [JsonConstructor] + public DataStoreInfoWrapper(DataStoreInfo dataStore) + { + DataStore = dataStore; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Datastores/DataStoreListWrapper.cs b/src/GeoTools.GeoServer/Models/Datastore/DataStoreListWrapper.cs similarity index 69% rename from src/GeoTools.GeoServer.Abstractions/Models/Datastores/DataStoreListWrapper.cs rename to src/GeoTools.GeoServer/Models/Datastore/DataStoreListWrapper.cs index 055c6ff..0a33001 100644 --- a/src/GeoTools.GeoServer.Abstractions/Models/Datastores/DataStoreListWrapper.cs +++ b/src/GeoTools.GeoServer/Models/Datastore/DataStoreListWrapper.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using GeoTools.GeoServer.Models.CatalogResponses; +using System.Collections.Generic; using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Datastore { /// /// Wrapper object in order to comply with current API encoding. /// - public class DataStoreListWrapper + internal class DataStoreListWrapper { [JsonPropertyName("dataStore")] public IList DataStore { get; } diff --git a/src/GeoTools.GeoServer/Models/Datastore/DataStoreSummaryWrapper.cs b/src/GeoTools.GeoServer/Models/Datastore/DataStoreSummaryWrapper.cs new file mode 100644 index 0000000..c1b6825 --- /dev/null +++ b/src/GeoTools.GeoServer/Models/Datastore/DataStoreSummaryWrapper.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace GeoTools.GeoServer.Models.Datastore +{ + /// + /// Wrapper object for DataStoreSummary, in order to comply with current API encoding. + /// + internal class DataStoreSummaryWrapper + { + [JsonPropertyName("dataStore")] + public DataStoreSummary DataStore { get; } + + [JsonConstructor] + public DataStoreSummaryWrapper(DataStoreSummary dataStore) + { + DataStore = dataStore; + } + } +} diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Datastores/DataStoresListResponse.cs b/src/GeoTools.GeoServer/Models/Datastore/DataStoresListResponse.cs similarity index 80% rename from src/GeoTools.GeoServer.Abstractions/Models/Datastores/DataStoresListResponse.cs rename to src/GeoTools.GeoServer/Models/Datastore/DataStoresListResponse.cs index 25e163f..a149605 100644 --- a/src/GeoTools.GeoServer.Abstractions/Models/Datastores/DataStoresListResponse.cs +++ b/src/GeoTools.GeoServer/Models/Datastore/DataStoresListResponse.cs @@ -1,11 +1,11 @@ using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Datastore { /// /// Datastores. /// - public class DataStoresListResponse + internal class DataStoresListResponse { [JsonPropertyName("dataStores")] public DataStoreListWrapper DataStores { get; } diff --git a/src/GeoTools.GeoServer/Models/Workspace/GetWorkspaceResponse.cs b/src/GeoTools.GeoServer/Models/Workspace/GetWorkspaceResponse.cs index c6fc0aa..6cf8e2d 100644 --- a/src/GeoTools.GeoServer/Models/Workspace/GetWorkspaceResponse.cs +++ b/src/GeoTools.GeoServer/Models/Workspace/GetWorkspaceResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Workspace { internal class GetWorkspaceResponse { diff --git a/src/GeoTools.GeoServer/Models/Workspace/WorkspaceWrapper.cs b/src/GeoTools.GeoServer/Models/Workspace/WorkspaceWrapper.cs index 584b4c7..4c49596 100644 --- a/src/GeoTools.GeoServer/Models/Workspace/WorkspaceWrapper.cs +++ b/src/GeoTools.GeoServer/Models/Workspace/WorkspaceWrapper.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Workspace { /// /// Wrapper object around Workspace, in order to conform to how XStream serializes to JSON in GeoServer. diff --git a/src/GeoTools.GeoServer/Models/Workspace/WorkspacesResponse.cs b/src/GeoTools.GeoServer/Models/Workspace/WorkspacesResponse.cs index 46d2b90..cfd04be 100644 --- a/src/GeoTools.GeoServer/Models/Workspace/WorkspacesResponse.cs +++ b/src/GeoTools.GeoServer/Models/Workspace/WorkspacesResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace GeoTools.GeoServer.Models +namespace GeoTools.GeoServer.Models.Workspace { internal class WorkspacesResponse { diff --git a/src/GeoTools.GeoServer/Resources/Messages.Designer.cs b/src/GeoTools.GeoServer/Resources/Messages.Designer.cs index c4530e3..36e3e08 100644 --- a/src/GeoTools.GeoServer/Resources/Messages.Designer.cs +++ b/src/GeoTools.GeoServer/Resources/Messages.Designer.cs @@ -168,6 +168,15 @@ internal static string Request_409Conflict { } } + /// + /// Looks up a localized string similar to Request {0} failed with response 500 InternalServerError.. + /// + internal static string Request_500InternalServerError { + get { + return ResourceManager.GetString("Request_500InternalServerError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Value {0} doesn't match the expected value {1}.. /// diff --git a/src/GeoTools.GeoServer/Resources/Messages.resx b/src/GeoTools.GeoServer/Resources/Messages.resx index f5b61eb..db95c86 100644 --- a/src/GeoTools.GeoServer/Resources/Messages.resx +++ b/src/GeoTools.GeoServer/Resources/Messages.resx @@ -153,6 +153,9 @@ Request {0} failed with response 409 Conflict. + + Request {0} failed with response 500 InternalServerError. + Value {0} doesn't match the expected value {1}. diff --git a/src/GeoTools.GeoServer/Services/DatastoreService.cs b/src/GeoTools.GeoServer/Services/DatastoreService.cs index ad2e2bb..70075ac 100644 --- a/src/GeoTools.GeoServer/Services/DatastoreService.cs +++ b/src/GeoTools.GeoServer/Services/DatastoreService.cs @@ -1,20 +1,21 @@ -using Microsoft.Extensions.DependencyInjection; +using GeoTools.GeoServer.Helpers; +using GeoTools.GeoServer.Helpers.Converters; +using GeoTools.GeoServer.Models; +using GeoTools.GeoServer.Models.CatalogResponses; +using GeoTools.GeoServer.Models.Datastore; +using GeoTools.GeoServer.Resources; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; -using System.Text; -using System.Text.Json.Serialization; +using System.Net.Http.Json; using System.Text.Json; -using System.Threading.Tasks; -using GeoTools.GeoServer.Models; +using System.Text.Json.Serialization; using System.Threading; -using GeoTools.GeoServer.Helpers; -using GeoTools.GeoServer.Resources; -using System.Net.Http.Json; -using System.Net; -using System.Xml.Linq; +using System.Threading.Tasks; namespace GeoTools.GeoServer.Services { @@ -35,9 +36,207 @@ public DatastoreService(IHttpClientFactory httpClientFactory, IServiceProvider p PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, + Converters = + { + new ConnectionParametersConverter(), + } }; } + public async Task> CreateDatastoreAsync(string workspaceName, DataStoreInfo datastoreInfo, CancellationToken token) + { + using (HttpClient client = _httpClientFactory.CreateClient(GeoServerOptions.HttpClientName)) + { + try + { + var request = new HttpRequestMessage( + HttpMethod.Post, + $"workspaces/{workspaceName}/datastores") + { + Content = JsonContent.Create(new DataStoreInfoWrapper(datastoreInfo)) + }; + + var response = await client.SendAsync(request, token); + + switch (response.StatusCode) + { + case HttpStatusCode.Created: + var result = await response.Content.ReadAsStringAsync(); + if (result != datastoreInfo.Name) + { + throw new GeoServerClientException((int)response.StatusCode, null, + new ArgumentException(string.Format( + Messages.Value_ComparisonMismatch, + datastoreInfo.Name, + result))); + } + else if (response.Headers.Location == null) + { + throw new GeoServerClientException((int)response.StatusCode, null, + new ArgumentException(string.Format( + Messages.Value_Null, + nameof(response.Headers.Location)))); + } + else + { + _logger.LogInformation(string.Format( + Messages.Request_201Created, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, response.Headers.Location); + } + case HttpStatusCode.InternalServerError: + _logger.LogInformation(string.Format( + Messages.Request_500InternalServerError, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, null); + case HttpStatusCode.Unauthorized: + throw new GeoServerClientException((int)response.StatusCode, null, + new UnauthorizedAccessException(string.Format( + Messages.Request_401Unauthorized, + request.RequestUri))); + default: + throw new GeoServerClientException((int)response.StatusCode, null, + new ArgumentOutOfRangeException( + nameof(response.StatusCode), + response.StatusCode, + string.Format( + Messages.Value_OutOfRange, + nameof(response.StatusCode), + "{201,401,500}"))); + } + } + catch (GeoServerClientException e) + { + if (_options.IgnoreServerErrors) + { + _logger.LogWarning(e, nameof(CreateDatastoreAsync)); + return new GeoServerResponse(e.StatusCode, null); + } + else + { + _logger.LogError(e, nameof(CreateDatastoreAsync)); + throw e.InnerException; + } + } + } + } + + public async Task> DeleteDatastoreAsync(string workspaceName, string datastoreName, bool? recurse, CancellationToken token) + { + using (HttpClient client = _httpClientFactory.CreateClient(GeoServerOptions.HttpClientName)) + { + try + { + var request = new HttpRequestMessage( + HttpMethod.Delete, + $"workspaces/{workspaceName}/datastores/{datastoreName}" + (recurse.HasValue ? $"?recurse={recurse}" : "")); + + var response = await client.SendAsync(request, token); + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + _logger.LogInformation(string.Format( + Messages.Request_200OK, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, true); + case HttpStatusCode.NotFound: + _logger.LogInformation(string.Format( + Messages.Request_404NotFound, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, false); + case HttpStatusCode.Forbidden: + _logger.LogInformation(string.Format( + Messages.Request_403Forbidden, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, false); + case HttpStatusCode.Unauthorized: + throw new GeoServerClientException((int)response.StatusCode, null, + new UnauthorizedAccessException(string.Format( + Messages.Request_401Unauthorized, + request.RequestUri))); + default: + throw new GeoServerClientException((int)response.StatusCode, null, + new ArgumentOutOfRangeException( + nameof(response.StatusCode), + response.StatusCode, + string.Format( + Messages.Value_OutOfRange, + nameof(response.StatusCode), + "{200,401,403,404}"))); + } + } + catch (GeoServerClientException e) + { + if (_options.IgnoreServerErrors) + { + _logger.LogWarning(e, nameof(DeleteDatastoreAsync)); + return new GeoServerResponse(e.StatusCode, false); + } + else + { + _logger.LogError(e, nameof(DeleteDatastoreAsync)); + throw e.InnerException; + } + } + } + } + + public async Task> GetDatastoreAsync(string workspaceName, string datastoreName, CancellationToken token) + { + using (HttpClient client = _httpClientFactory.CreateClient(GeoServerOptions.HttpClientName)) + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, $"workspaces/{workspaceName}/datastores/{datastoreName}?quietOnNotFound={_options.QuietIfNotFound}"); + + var response = await client.SendAsync(request, token); + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + var result = await response.Content.ReadFromJsonAsync(_jsonOpt, token); + _logger.LogInformation(string.Format( + Messages.Request_200OK, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, result.DataStore); + case HttpStatusCode.NotFound: + _logger.LogInformation(string.Format( + Messages.Request_404NotFound, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, null); + case HttpStatusCode.Unauthorized: + throw new GeoServerClientException((int)response.StatusCode, null, + new UnauthorizedAccessException(string.Format( + Messages.Request_401Unauthorized, + request.RequestUri))); + default: + throw new GeoServerClientException((int)response.StatusCode, null, + new ArgumentOutOfRangeException( + nameof(response.StatusCode), + response.StatusCode, + string.Format( + Messages.Value_OutOfRange, + nameof(response.StatusCode), + "{200,401,404}"))); + } + } + catch (GeoServerClientException e) + { + if (_options.IgnoreServerErrors) + { + _logger.LogWarning(e, nameof(GetDatastoreAsync)); + return new GeoServerResponse(e.StatusCode, null); + } + else + { + _logger.LogError(e, nameof(GetDatastoreAsync)); + throw e.InnerException; + } + } + } + } + public async Task>> GetDatastoresAsync(string workspaceName, CancellationToken token) { using (HttpClient client = _httpClientFactory.CreateClient(GeoServerOptions.HttpClientName)) @@ -92,5 +291,69 @@ public async Task>> GetDatastoresAsync(string } } } + + public async Task> UpdateDatastoreAsync(string workspaceName, DataStoreInfo datastore, CancellationToken token) + { + using (HttpClient client = _httpClientFactory.CreateClient(GeoServerOptions.HttpClientName)) + { + try + { + var request = new HttpRequestMessage( + HttpMethod.Put, + $"workspaces/{workspaceName}/datastores/{datastore.Name}") + { + Content = JsonContent.Create(new DataStoreInfoWrapper(datastore)), + }; + + var response = await client.SendAsync(request, token); + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + _logger.LogInformation(string.Format( + Messages.Request_200OK, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, true); + case HttpStatusCode.NotFound: + _logger.LogInformation(string.Format( + Messages.Request_404NotFound, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, false); + case HttpStatusCode.MethodNotAllowed: + _logger.LogInformation(string.Format( + Messages.Request_405MethodNotAllowed, + request.RequestUri)); + return new GeoServerResponse((int)response.StatusCode, false); + case HttpStatusCode.Unauthorized: + throw new GeoServerClientException((int)response.StatusCode, null, + new UnauthorizedAccessException(string.Format( + Messages.Request_401Unauthorized, + request.RequestUri))); + default: + throw new GeoServerClientException((int)response.StatusCode, null, + new ArgumentOutOfRangeException( + nameof(response.StatusCode), + response.StatusCode, + string.Format( + Messages.Value_OutOfRange, + nameof(response.StatusCode), + "{200,401,404,405}"))); + } + } + catch (GeoServerClientException e) + { + if (_options.IgnoreServerErrors) + { + _logger.LogWarning(e, nameof(UpdateDatastoreAsync)); + return new GeoServerResponse(e.StatusCode, false); + } + else + { + _logger.LogError(e, nameof(UpdateDatastoreAsync)); + throw e.InnerException; + } + } + } + } } } diff --git a/src/GeoTools.GeoServer/Services/WorkspaceService.cs b/src/GeoTools.GeoServer/Services/WorkspaceService.cs index 5c03005..3b3740e 100644 --- a/src/GeoTools.GeoServer/Services/WorkspaceService.cs +++ b/src/GeoTools.GeoServer/Services/WorkspaceService.cs @@ -1,5 +1,7 @@ using GeoTools.GeoServer.Helpers; using GeoTools.GeoServer.Models; +using GeoTools.GeoServer.Models.CatalogResponses; +using GeoTools.GeoServer.Models.Workspace; using GeoTools.GeoServer.Resources; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/tests/GeoTools.GeoServer.Tests/Services/DatastoreServiceTests.cs b/tests/GeoTools.GeoServer.Tests/Services/DatastoreServiceTests.cs new file mode 100644 index 0000000..bcb0c47 --- /dev/null +++ b/tests/GeoTools.GeoServer.Tests/Services/DatastoreServiceTests.cs @@ -0,0 +1,175 @@ +using GeoTools.GeoServer.Extensions; +using GeoTools.GeoServer.Models.Datastore; +using GeoTools.GeoServer.Models.Datastore.Sources; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace GeoTools.GeoServer.Tests.Services +{ + public class DatastoreServiceTests + { + private readonly ITestOutputHelper _output; + private readonly IConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + + [Fact] + public async Task GivenDefaultInstallation_GetDatastores() + { + CancellationToken token = default; + + var service = _serviceProvider.GetRequiredService(); + + var responseWrapper = await service.GetDatastoresAsync("topp", token); + + Assert.NotNull(responseWrapper); + Assert.Equal(200, responseWrapper.StatusCode); + + var response = responseWrapper.Response; + Assert.NotEmpty(response); + Assert.All(response, x => + { + Assert.False(string.IsNullOrWhiteSpace(x.Name)); + Assert.False(string.IsNullOrWhiteSpace(x.Href)); + }); + } + + [Fact] + public async Task GivenDefaultInstallation_CreateDeleteDatastore() + { + CancellationToken token = default; + const string CreateDatastoreName = "states1"; + + var workspaceService = _serviceProvider.GetRequiredService(); + var datastoreService = _serviceProvider.GetRequiredService(); + + var workspaceWrapper = await workspaceService.GetWorkspaceAsync("topp", token); + + Assert.NotNull(workspaceWrapper); + Assert.Equal(200, workspaceWrapper.StatusCode); + + var createResponseWrapper = await datastoreService.CreateDatastoreAsync( + workspaceWrapper.Response.Name, + new DataStoreInfo( + CreateDatastoreName, "MyDesc", true, + new ShapefileConnectionParameters(new Uri("file:///data/shapefiles/states.shp"), new Uri("http://www.openplans.org/topp")), + false), + token); + + Assert.NotNull(createResponseWrapper); + Assert.Equal(201, createResponseWrapper.StatusCode); + + var createResponse = createResponseWrapper.Response; + Assert.NotNull(createResponse); + + var getResponseWrapper = await datastoreService.GetDatastoreAsync(workspaceWrapper.Response.Name, CreateDatastoreName, token); + + Assert.NotNull(getResponseWrapper); + Assert.Equal(200, getResponseWrapper.StatusCode); + + var getResponse = getResponseWrapper.Response; + Assert.NotNull(getResponse); + Assert.Equal(CreateDatastoreName, getResponse.Name); + Assert.Equal("MyDesc", getResponse.Description); + // TODO: Should other fields be checked? + Assert.True(getResponse.Enabled); + + var deleteResponseWrapper = await datastoreService.DeleteDatastoreAsync(workspaceWrapper.Response.Name, CreateDatastoreName, false, token); + + Assert.NotNull(deleteResponseWrapper); + Assert.Equal(200, deleteResponseWrapper.StatusCode); + + var deleteResponse = deleteResponseWrapper.Response; + Assert.True(deleteResponse); + } + + [Fact] + public async Task GivenDefaultInstallation_CreateUpdateDeleteDatastore() + { + CancellationToken token = default; + const string CreateDatastoreName = "states1"; + + var workspaceService = _serviceProvider.GetRequiredService(); + var datastoreService = _serviceProvider.GetRequiredService(); + + var workspaceWrapper = await workspaceService.GetWorkspaceAsync("topp", token); + + Assert.NotNull(workspaceWrapper); + Assert.Equal(200, workspaceWrapper.StatusCode); + + var datastoreInfoCreate = new DataStoreInfo( + CreateDatastoreName, "MyDesc", true, + new ShapefileConnectionParameters(new Uri("file:///data/shapefiles/states.shp"), new Uri("http://www.openplans.org/topp")), + false); + + var createResponseWrapper = await datastoreService.CreateDatastoreAsync( + workspaceWrapper.Response.Name, + datastoreInfoCreate, + token); + + Assert.NotNull(createResponseWrapper); + Assert.Equal(201, createResponseWrapper.StatusCode); + + var createResponse = createResponseWrapper.Response; + Assert.NotNull(createResponse); + + var datastoreInfoUpdate = new DataStoreInfo( + CreateDatastoreName, "MyDesc1", true, + new ShapefileConnectionParameters(new Uri("file:///data/shapefiles/states.shp"), new Uri("http://www.openplans.org/topp")), + false); + + var updateResponseWrapper = await datastoreService.UpdateDatastoreAsync( + workspaceWrapper.Response.Name, + datastoreInfoUpdate, + token); + + Assert.NotNull(updateResponseWrapper); + Assert.Equal(200, updateResponseWrapper.StatusCode); + + var updateResponse = updateResponseWrapper.Response; + Assert.True(updateResponse); + + var getResponseWrapper = await datastoreService.GetDatastoreAsync(workspaceWrapper.Response.Name, CreateDatastoreName, token); + + Assert.NotNull(getResponseWrapper); + Assert.Equal(200, getResponseWrapper.StatusCode); + + var getResponse = getResponseWrapper.Response; + Assert.NotNull(getResponse); + Assert.Equal(CreateDatastoreName, getResponse.Name); + Assert.Equal("MyDesc1", getResponse.Description); + // TODO: Should other fields be checked? + Assert.True(getResponse.Enabled); + + var deleteResponseWrapper = await datastoreService.DeleteDatastoreAsync(workspaceWrapper.Response.Name, CreateDatastoreName, false, token); + + Assert.NotNull(deleteResponseWrapper); + Assert.Equal(200, deleteResponseWrapper.StatusCode); + + var deleteResponse = deleteResponseWrapper.Response; + Assert.True(deleteResponse); + } + + public DatastoreServiceTests(ITestOutputHelper output) + { + _output = output; + + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection() + .Build(); + + _serviceProvider = new ServiceCollection() + .AddGeoServer(options => + { + options.BaseAddress = new Uri("http://localhost:8080/geoserver/rest/"); + options.AuthorizationHeaderValue = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:geoserver")); + }) + .AddLogging((builder) => builder.AddXUnit(_output, options => + { + options.IncludeScopes = true; + })) + .BuildServiceProvider(); + } + } +} diff --git a/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs b/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs index 2329b2d..9d9474c 100644 --- a/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs +++ b/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs @@ -1,4 +1,5 @@ using GeoTools.GeoServer.Extensions; +using GeoTools.GeoServer.Models.Workspace; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -26,9 +27,10 @@ public async Task GivenDefaultInstallation_GetWorkspace() var response = responseWrapper.Response; Assert.NotNull(response); Assert.False(string.IsNullOrWhiteSpace(response.Name)); - Assert.False(string.IsNullOrWhiteSpace(response.CoverageStores)); - Assert.False(string.IsNullOrWhiteSpace(response.DataStores)); - Assert.False(string.IsNullOrWhiteSpace(response.WmsStores)); + Assert.False(string.IsNullOrWhiteSpace(response.CoverageStores?.ToString())); + Assert.False(string.IsNullOrWhiteSpace(response.DataStores?.ToString())); + Assert.False(string.IsNullOrWhiteSpace(response.WmsStores?.ToString())); + Assert.False(string.IsNullOrWhiteSpace(response.WmtsStores?.ToString())); responseWrapper = await service.GetWorkspaceAsync("ne1", token); Assert.NotNull(responseWrapper); diff --git a/tests/GeoTools.GeoServer.Tests/Usings.cs b/tests/GeoTools.GeoServer.Tests/Usings.cs index 0a5984b..66a791a 100644 --- a/tests/GeoTools.GeoServer.Tests/Usings.cs +++ b/tests/GeoTools.GeoServer.Tests/Usings.cs @@ -1,4 +1,3 @@ -global using GeoTools.GeoServer.Models; global using GeoTools.GeoServer.Services; global using Xunit; global using Xunit.Abstractions;