diff --git a/.gitattributes b/.gitattributes
index 1ff0c42..682d4fa 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,63 +1,19 @@
-###############################################################################
-# Set default behavior to automatically normalize line endings.
-###############################################################################
* text=auto
-###############################################################################
-# Set default behavior for command prompt diff.
-#
-# This is need for earlier builds of msysgit that does not have it on by
-# default for csharp files.
-# Note: This is only used by command line
-###############################################################################
-#*.cs diff=csharp
+# Force bash scripts to always use lf line endings so that if a repo is accessed
+# in Unix via a file share from Windows, the scripts will work.
+*.in text eol=lf
+*.sh text eol=lf
-###############################################################################
-# Set the merge driver for project and solution files
-#
-# Merging from the command prompt will add diff markers to the files if there
-# are conflicts (Merging from VS is not affected by the settings below, in VS
-# the diff markers are never inserted). Diff markers may cause the following
-# file extensions to fail to load in VS. An alternative would be to treat
-# these files as binary and thus will always conflict and require user
-# intervention with every merge. To do so, just uncomment the entries below
-###############################################################################
-#*.sln merge=binary
-#*.csproj merge=binary
-#*.vbproj merge=binary
-#*.vcxproj merge=binary
-#*.vcproj merge=binary
-#*.dbproj merge=binary
-#*.fsproj merge=binary
-#*.lsproj merge=binary
-#*.wixproj merge=binary
-#*.modelproj merge=binary
-#*.sqlproj merge=binary
-#*.wwaproj merge=binary
+# Likewise, force cmd and batch scripts to always use crlf
+*.cmd text eol=crlf
+*.bat text eol=crlf
-###############################################################################
-# behavior for image files
-#
-# image files are treated as binary by default.
-###############################################################################
-#*.jpg binary
-#*.png binary
-#*.gif binary
+*.cs text=auto diff=csharp
+*.csproj text=auto
+*.sln text=auto
+*.resx text=auto
+*.xml text=auto
+*.txt text=auto
-###############################################################################
-# diff behavior for common document formats
-#
-# Convert binary document formats to text before diffing them. This feature
-# is only available from the command line. Turn it on by uncommenting the
-# entries below.
-###############################################################################
-#*.doc diff=astextplain
-#*.DOC diff=astextplain
-#*.docx diff=astextplain
-#*.DOCX diff=astextplain
-#*.dot diff=astextplain
-#*.DOT diff=astextplain
-#*.pdf diff=astextplain
-#*.PDF diff=astextplain
-#*.rtf diff=astextplain
-#*.RTF diff=astextplain
+packages/ binary
\ No newline at end of file
diff --git a/GeoTools.GeoServer/Class1.cs b/GeoTools.GeoServer/Class1.cs
deleted file mode 100644
index 9079c34..0000000
--- a/GeoTools.GeoServer/Class1.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace GeoTools.GeoServer
-{
- public class Class1
- {
-
- }
-}
diff --git a/GeoTools.sln b/GeoTools.sln
index 77dc30e..f636330 100644
--- a/GeoTools.sln
+++ b/GeoTools.sln
@@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33020.496
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoTools.GeoServer", "GeoTools.GeoServer\GeoTools.GeoServer.csproj", "{46D1A21A-2C2F-46CF-899C-90D3D8E3B747}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeoTools.GeoServer", "src\GeoTools.GeoServer\GeoTools.GeoServer.csproj", "{46D1A21A-2C2F-46CF-899C-90D3D8E3B747}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeoTools.GeoServer.Abstractions", "src\GeoTools.GeoServer.Abstractions\GeoTools.GeoServer.Abstractions.csproj", "{CD6C97F4-922E-4C71-AADF-926008764840}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoTools.GeoServer.Tests", "tests\GeoTools.GeoServer.Tests\GeoTools.GeoServer.Tests.csproj", "{AA3ECC68-7ACF-4625-896F-96948198ECFE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +19,14 @@ Global
{46D1A21A-2C2F-46CF-899C-90D3D8E3B747}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46D1A21A-2C2F-46CF-899C-90D3D8E3B747}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46D1A21A-2C2F-46CF-899C-90D3D8E3B747}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD6C97F4-922E-4C71-AADF-926008764840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD6C97F4-922E-4C71-AADF-926008764840}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD6C97F4-922E-4C71-AADF-926008764840}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD6C97F4-922E-4C71-AADF-926008764840}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA3ECC68-7ACF-4625-896F-96948198ECFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA3ECC68-7ACF-4625-896F-96948198ECFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA3ECC68-7ACF-4625-896F-96948198ECFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA3ECC68-7ACF-4625-896F-96948198ECFE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/GeoTools.GeoServer/GeoTools.GeoServer.csproj b/src/GeoTools.GeoServer.Abstractions/GeoTools.GeoServer.Abstractions.csproj
similarity index 67%
rename from GeoTools.GeoServer/GeoTools.GeoServer.csproj
rename to src/GeoTools.GeoServer.Abstractions/GeoTools.GeoServer.Abstractions.csproj
index 9f5c4f4..66646c1 100644
--- a/GeoTools.GeoServer/GeoTools.GeoServer.csproj
+++ b/src/GeoTools.GeoServer.Abstractions/GeoTools.GeoServer.Abstractions.csproj
@@ -4,4 +4,8 @@
netstandard2.0
+
+
+
+
diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/GetWorkspaceResponse.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/GetWorkspaceResponse.cs
new file mode 100644
index 0000000..0fda459
--- /dev/null
+++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/GetWorkspaceResponse.cs
@@ -0,0 +1,12 @@
+namespace GeoTools.GeoServer.Models
+{
+ public class GetWorkspaceResponse
+ {
+ public WorkspaceSummary Workspace { get; }
+
+ public GetWorkspaceResponse(WorkspaceSummary workspace)
+ {
+ Workspace = workspace;
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs
new file mode 100644
index 0000000..6affe17
--- /dev/null
+++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceInfo.cs
@@ -0,0 +1,30 @@
+namespace GeoTools.GeoServer.Models
+{
+ ///
+ /// Workspace.
+ ///
+ public class WorkspaceInfo
+ {
+ ///
+ /// Name of the workspace.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Is the workspace content isolated so it is not included as part of
+ /// global web services.
+ ///
+ ///
+ /// Isolated workspaces content is only visible and queryable in the
+ /// context of a virtual service bound to the isolated workspace.
+ ///
+ /// False by default.
+ public bool Isolated { get; }
+
+ public WorkspaceInfo(string name, bool isolated = false)
+ {
+ Name = name;
+ Isolated = isolated;
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs
new file mode 100644
index 0000000..a589138
--- /dev/null
+++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceSummary.cs
@@ -0,0 +1,48 @@
+namespace GeoTools.GeoServer.Models
+{
+ ///
+ /// Workspace Response.
+ ///
+ public class WorkspaceSummary
+ {
+ ///
+ /// Name of workspace.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Is the workspace content isolated so it is not included as part of
+ /// global web services.
+ ///
+ ///
+ /// Isolated workspaces content is only visible and queryable in the
+ /// context of a virtual service bound to the isolated workspace.
+ ///
+ /// False by default.
+ public bool Isolated { get; }
+
+ ///
+ /// URL to Datas tores in this workspace.
+ ///
+ public string DataStores { get; }
+
+ ///
+ /// URL to Coverage stores in this workspace.
+ ///
+ public string CoverageStores { get; }
+
+ ///
+ /// URL to WMS stores in this workspace.
+ ///
+ public string WmsStores { get; }
+
+ public WorkspaceSummary(string name, string dataStores, string coverageStores, string wmsStores, bool isolated = false)
+ {
+ Name = name;
+ Isolated = isolated;
+ DataStores = dataStores;
+ CoverageStores = coverageStores;
+ WmsStores = wmsStores;
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceWrapper.cs b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceWrapper.cs
new file mode 100644
index 0000000..85f6088
--- /dev/null
+++ b/src/GeoTools.GeoServer.Abstractions/Models/Workspace/WorkspaceWrapper.cs
@@ -0,0 +1,15 @@
+namespace GeoTools.GeoServer.Models
+{
+ ///
+ /// Wrapper object around Workspace, in order to conform to how XStream serializes to JSON in GeoServer.
+ ///
+ public class WorkspaceWrapper
+ {
+ public WorkspaceInfo Workspace { get; }
+
+ public WorkspaceWrapper(WorkspaceInfo workspace)
+ {
+ Workspace = workspace;
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs b/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs
new file mode 100644
index 0000000..35e54f0
--- /dev/null
+++ b/src/GeoTools.GeoServer.Abstractions/Services/IWorkspaceService.cs
@@ -0,0 +1,17 @@
+using GeoTools.GeoServer.Models;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GeoTools.GeoServer.Services
+{
+ public interface IWorkspaceService
+ {
+ ///
+ /// Retrieve a Workspace.
+ ///
+ /// The name of the workspace to fetch.
+ ///
+ /// Retrieves a single workspace definition.
+ Task GetWorkspaceAsync(string name, CancellationToken token);
+ }
+}
diff --git a/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs b/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs
new file mode 100644
index 0000000..99f7182
--- /dev/null
+++ b/src/GeoTools.GeoServer/Extensions/GeoServerExtensions.cs
@@ -0,0 +1,68 @@
+using GeoTools.GeoServer.Helpers.Options;
+using GeoTools.GeoServer.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+
+namespace GeoTools.GeoServer.Extensions
+{
+ public static class GeoServerExtensions
+ {
+ public static IServiceCollection AddGeoServer(this IServiceCollection services)
+ {
+ return services.AddGeoServer(options => { });
+ }
+
+ public static IServiceCollection AddGeoServer(this IServiceCollection services, Action configureOptions)
+ {
+ return AddGeoServer(services, (options, _) => configureOptions(options));
+ }
+
+ public static IServiceCollection AddGeoServer(this IServiceCollection services, Action configureOptions)
+ {
+ services
+ .AddSingleton, ValidateGeoServerOptions>()
+ .AddOptions()
+ .Configure(configureOptions);
+
+ services
+ .AddHttpClient(GeoServerOptions.HttpClientName, ConfigureHttpClient);
+
+ return services
+ .AddTransient();
+ }
+
+ private static void ConfigureHttpClient(IServiceProvider provider, HttpClient client)
+ {
+ var optionsWrapper = provider.GetService>();
+
+ if (optionsWrapper != null)
+ {
+ var options = optionsWrapper.Value;
+
+ if (options.ConfigureHttpClient != null)
+ {
+ options.ConfigureHttpClient(provider, client);
+ }
+ else
+ {
+ client.BaseAddress = options.BaseAddress;
+
+ if (options.AuthorizationHeaderValue != null)
+ {
+ var authTokens = options.AuthorizationHeaderValue.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ var authSchema = authTokens[0];
+ var authParameter = string.Join(" ", authTokens.Skip(1));
+
+ client.DefaultRequestHeaders.Authorization = string.IsNullOrEmpty(authParameter)
+ ? new AuthenticationHeaderValue(authSchema)
+ : new AuthenticationHeaderValue(authSchema, authParameter);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer/GeoTools.GeoServer.csproj b/src/GeoTools.GeoServer/GeoTools.GeoServer.csproj
new file mode 100644
index 0000000..1008a27
--- /dev/null
+++ b/src/GeoTools.GeoServer/GeoTools.GeoServer.csproj
@@ -0,0 +1,36 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ ExceptionMessages.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ ExceptionMessages.Designer.cs
+
+
+
+
diff --git a/src/GeoTools.GeoServer/Helpers/Options/ValidateGeoServerOptions.cs b/src/GeoTools.GeoServer/Helpers/Options/ValidateGeoServerOptions.cs
new file mode 100644
index 0000000..6a2d030
--- /dev/null
+++ b/src/GeoTools.GeoServer/Helpers/Options/ValidateGeoServerOptions.cs
@@ -0,0 +1,37 @@
+using GeoTools.GeoServer.Resources;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+
+namespace GeoTools.GeoServer.Helpers.Options
+{
+ internal class ValidateGeoServerOptions : IValidateOptions
+ {
+ public ValidateOptionsResult Validate(string name, GeoServerOptions options)
+ {
+ var failures = new List();
+
+ if (options is null)
+ return ValidateOptionsResult.Fail(ExceptionMessages.Options_Null);
+
+ if (options.ConfigureHttpClient != null)
+ return ValidateOptionsResult.Success;
+
+ if (options.BaseAddress == null)
+ failures.Add(ExceptionMessages.Options_BaseAddressNull);
+ else if (options.BaseAddress.IsFile)
+ failures.Add(ExceptionMessages.Options_BaseAddressFile);
+
+ if (options.AuthorizationHeaderValue != null &&
+ options.AuthorizationHeaderValue.Split(
+ new char[] { ' ' },
+ StringSplitOptions.RemoveEmptyEntries).Length < 1)
+ failures.Add(ExceptionMessages.Options_AuthorizationHeaderValueWrongFormat);
+
+ if (failures.Count > 0)
+ return ValidateOptionsResult.Fail(failures);
+ else
+ return ValidateOptionsResult.Success;
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer/Options/GeoServerOptions.cs b/src/GeoTools.GeoServer/Options/GeoServerOptions.cs
new file mode 100644
index 0000000..0c19c7a
--- /dev/null
+++ b/src/GeoTools.GeoServer/Options/GeoServerOptions.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Net.Http;
+
+namespace GeoTools.GeoServer
+{
+ public class GeoServerOptions
+ {
+ ///
+ /// If a GeoServer error, like 400/401/500 occurs, return null instead
+ /// of throwing an exception.
+ ///
+ /// True by default.
+ public bool IgnoreServerErrors { get; set; } = true;
+
+ ///
+ /// Avoids logging an exception when the workspace is not present. Note
+ /// that 404 status code will still be returned.
+ ///
+ /// True by default.
+ public bool QuietIfNotFound { get; set; } = true;
+
+ #region HttpClient
+
+ ///
+ /// Base uri address of the server,
+ /// e.g. http://localhost:8080/geoserver/rest/.
+ ///
+ public Uri BaseAddress { get; set; }
+
+ ///
+ /// Full value of the Authorization header, e.g. Basic AABBCC.
+ ///
+ public string AuthorizationHeaderValue { get; set; }
+
+ ///
+ /// If not null, overrides other http-related options.
+ ///
+ public Action ConfigureHttpClient { get; set; }
+
+ ///
+ /// Name of the client to get from the factory.
+ ///
+ internal const string HttpClientName = nameof(GeoServer);
+
+ #endregion
+ }
+}
diff --git a/src/GeoTools.GeoServer/README.md b/src/GeoTools.GeoServer/README.md
new file mode 100644
index 0000000..9a9ee91
--- /dev/null
+++ b/src/GeoTools.GeoServer/README.md
@@ -0,0 +1,3 @@
+# GeoTools.GeoServer
+
+This client library uses documentation from [the GeoServer community OpenAPI configuration](https://github.com/geoserver/geoserver/tree/main/src/community/rest-openapi/openapi/src/main/resources/org/geoserver/rest/openapi/1.0.0).
\ No newline at end of file
diff --git a/src/GeoTools.GeoServer/Resources/ExceptionMessages.Designer.cs b/src/GeoTools.GeoServer/Resources/ExceptionMessages.Designer.cs
new file mode 100644
index 0000000..513d194
--- /dev/null
+++ b/src/GeoTools.GeoServer/Resources/ExceptionMessages.Designer.cs
@@ -0,0 +1,135 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace GeoTools.GeoServer.Resources {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ExceptionMessages {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ExceptionMessages() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GeoTools.GeoServer.Resources.ExceptionMessages", typeof(ExceptionMessages).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Authorization header value should consist of a schema with an optional parameter..
+ ///
+ internal static string Options_AuthorizationHeaderValueWrongFormat {
+ get {
+ return ResourceManager.GetString("Options_AuthorizationHeaderValueWrongFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The BaseAddress Uri can't point to a file..
+ ///
+ internal static string Options_BaseAddressFile {
+ get {
+ return ResourceManager.GetString("Options_BaseAddressFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The BaseAddress Uri can't be null if ConfigureHttpClient isn't specified..
+ ///
+ internal static string Options_BaseAddressNull {
+ get {
+ return ResourceManager.GetString("Options_BaseAddressNull", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Validated options model is null..
+ ///
+ internal static string Options_Null {
+ get {
+ return ResourceManager.GetString("Options_Null", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Request {0} failed with response 404 Not Found..
+ ///
+ internal static string Request_NotFound {
+ get {
+ return ResourceManager.GetString("Request_NotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Request {0} succeeded with response 200 OK..
+ ///
+ internal static string Request_OK {
+ get {
+ return ResourceManager.GetString("Request_OK", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Request {0} failed with response 401 Unauthorized..
+ ///
+ internal static string Request_Unauthorized {
+ get {
+ return ResourceManager.GetString("Request_Unauthorized", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Value {0} is out of supported range {1}..
+ ///
+ internal static string Value_OutOfRange {
+ get {
+ return ResourceManager.GetString("Value_OutOfRange", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/GeoTools.GeoServer/Resources/ExceptionMessages.resx b/src/GeoTools.GeoServer/Resources/ExceptionMessages.resx
new file mode 100644
index 0000000..00b4005
--- /dev/null
+++ b/src/GeoTools.GeoServer/Resources/ExceptionMessages.resx
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Authorization header value should consist of a schema with an optional parameter.
+
+
+ The BaseAddress Uri can't point to a file.
+
+
+ The BaseAddress Uri can't be null if ConfigureHttpClient isn't specified.
+
+
+ Validated options model is null.
+
+
+ Request {0} failed with response 404 Not Found.
+
+
+ Request {0} succeeded with response 200 OK.
+
+
+ Request {0} failed with response 401 Unauthorized.
+
+
+ Value {0} is out of supported range {1}.
+
+
\ No newline at end of file
diff --git a/src/GeoTools.GeoServer/Resources/ExceptionMessages.uk-UA.resx b/src/GeoTools.GeoServer/Resources/ExceptionMessages.uk-UA.resx
new file mode 100644
index 0000000..49ec2c5
--- /dev/null
+++ b/src/GeoTools.GeoServer/Resources/ExceptionMessages.uk-UA.resx
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Значення заголовка Authorization має містити схему та опціонально параметр.
+
+
+ Uri BaseAddress не може вказувати на файл.
+
+
+ Uri BaseAddress не може містити значення null якщо ConfigureHttpClient не присвоєно.
+
+
+ Валідована модель опцій має значення null.
+
+
\ No newline at end of file
diff --git a/src/GeoTools.GeoServer/Services/WorkspaceService.cs b/src/GeoTools.GeoServer/Services/WorkspaceService.cs
new file mode 100644
index 0000000..0f55582
--- /dev/null
+++ b/src/GeoTools.GeoServer/Services/WorkspaceService.cs
@@ -0,0 +1,89 @@
+using GeoTools.GeoServer.Models;
+using GeoTools.GeoServer.Resources;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GeoTools.GeoServer.Services
+{
+ public class WorkspaceService : IWorkspaceService
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger _logger;
+ private readonly GeoServerOptions _options;
+
+ public WorkspaceService(IHttpClientFactory httpClientFactory, IServiceProvider provider, IOptions options)
+ {
+ _httpClientFactory = httpClientFactory;
+ _logger = provider.GetService>();
+ _options = options.Value;
+ }
+
+ public async Task GetWorkspaceAsync(string name, CancellationToken token)
+ {
+ using (HttpClient client = _httpClientFactory.CreateClient(GeoServerOptions.HttpClientName))
+ {
+ try
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, $"workspaces/{name}?quietOnNotFound={_options.QuietIfNotFound}");
+ request.Headers.Accept.Clear();
+ request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+ var response = await client.SendAsync(request, token);
+
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ var result = await response.Content.ReadFromJsonAsync(cancellationToken: token);
+ _logger.LogInformation(string.Format(
+ ExceptionMessages.Request_OK,
+ request.RequestUri));
+ return result.Workspace;
+ case HttpStatusCode.NotFound:
+ _logger.LogInformation(string.Format(
+ ExceptionMessages.Request_NotFound,
+ request.RequestUri));
+ return null;
+ case HttpStatusCode.Unauthorized:
+ throw new UnauthorizedAccessException(string.Format(
+ ExceptionMessages.Request_Unauthorized,
+ request.RequestUri));
+ default:
+ throw new ArgumentOutOfRangeException(
+ nameof(response.StatusCode),
+ response.StatusCode,
+ string.Format(
+ ExceptionMessages.Value_OutOfRange,
+ nameof(response.StatusCode),
+ "{200,401,404}"));
+ }
+ }
+ catch (UnauthorizedAccessException e)
+ {
+ _logger.LogError(e, nameof(GetWorkspaceAsync));
+ throw;
+ }
+ catch (Exception e)
+ {
+ if (_options.IgnoreServerErrors)
+ {
+ _logger.LogWarning(e, nameof(GetWorkspaceAsync));
+ return null;
+ }
+ else
+ {
+ _logger.LogError(e, nameof(GetWorkspaceAsync));
+ throw;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/GeoTools.GeoServer.Tests/GeoTools.GeoServer.Tests.csproj b/tests/GeoTools.GeoServer.Tests/GeoTools.GeoServer.Tests.csproj
new file mode 100644
index 0000000..dce1ab3
--- /dev/null
+++ b/tests/GeoTools.GeoServer.Tests/GeoTools.GeoServer.Tests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs b/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs
new file mode 100644
index 0000000..3469f6d
--- /dev/null
+++ b/tests/GeoTools.GeoServer.Tests/Services/WorkspaceServiceTests.cs
@@ -0,0 +1,55 @@
+using GeoTools.GeoServer.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System.Globalization;
+using System.Text;
+
+namespace GeoTools.GeoServer.Tests.Services
+{
+ public class WorkspaceServiceTests
+ {
+ private readonly ITestOutputHelper _output;
+ private readonly IConfiguration _configuration;
+ private readonly IServiceProvider _serviceProvider;
+
+ [Fact]
+ public async Task GivenDefaultInstallation_GetWorkspace()
+ {
+ CancellationToken token = default;
+
+ var service = _serviceProvider.GetRequiredService();
+
+ var response = await service.GetWorkspaceAsync("ne", token);
+
+ 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.Null(await service.GetWorkspaceAsync("nen", token));
+ }
+
+ public WorkspaceServiceTests(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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GeoTools.GeoServer.Tests/Usings.cs b/tests/GeoTools.GeoServer.Tests/Usings.cs
new file mode 100644
index 0000000..50bb510
--- /dev/null
+++ b/tests/GeoTools.GeoServer.Tests/Usings.cs
@@ -0,0 +1,3 @@
+global using Xunit;
+global using Xunit.Abstractions;
+global using GeoTools.GeoServer.Services;
\ No newline at end of file