diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index bbb13d4e..0fcd04fb 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - global-json-file: global.json + dotnet-version: | + 6.0.x + 7.0.x - name: Install dependencies (hdf5) run: sudo apt install hdf5-tools -y - name: Restore dependencies diff --git a/global.json b/global.json deleted file mode 100644 index 2bf3b167..00000000 --- a/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sdk": { - "version": "6.0.406" - } -} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example0.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example0.json new file mode 100644 index 00000000..d6889999 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example0.json @@ -0,0 +1,15 @@ +{ + "filter": { + "op": "=", + "args": [ + { + "property": "id" + }, + "LC08_L1TP_060247_20180905_20180912_01_T1_L1TP" + ] + }, + "result": + { + "{http://a9.com/-/opensearch/extensions/geo/1.0/}uid": "LC08_L1TP_060247_20180905_20180912_01_T1_L1TP" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example1.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example1.json new file mode 100644 index 00000000..7ddad5c4 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example1.json @@ -0,0 +1,30 @@ +{ + "filter": { + "op": "and", + "args": [ + { + "op": "=", + "args": [ + { + "property": "id" + }, + "S2A_60HWB_20201111_0_L2A" + ] + }, + { + "op": "=", + "args": [ + { + "property": "collection" + }, + "sentinel-s2-l2a-cogs" + ] + } + ] + }, + "result": + { + "{http://a9.com/-/opensearch/extensions/geo/1.0/}uid": "S2A_60HWB_20201111_0_L2A", + "{http://a9.com/-/opensearch/extensions/eo/1.0/}parentIdentifier": "sentinel-s2-l2a-cogs" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example10.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example10.json new file mode 100644 index 00000000..6152c27c --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example10.json @@ -0,0 +1,16 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "between", + "args": [ + { + "property": "eo:cloud_cover" + }, + 0, + 50 + ] + }, + "result": { + "{http://a9.com/-/opensearch/extensions/eo/1.0/}cloudCover": "[0,50]" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example11.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example11.json new file mode 100644 index 00000000..0fbacffc --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example11.json @@ -0,0 +1,11 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "like", + "args": [ + { "property": "mission" }, + "sentinel%" + ] + }, + "exception": "System.NotSupportedException" +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example12.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example12.json new file mode 100644 index 00000000..08eb1031 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example12.json @@ -0,0 +1,19 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "in", + "args": [ + { + "property": "keywords" + }, + [ + "fire", + "forest", + "wildfire" + ] + ] + }, + "result": { + "{http://purl.org/dc/terms/}subject": "{fire,forest,wildfire}" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example2.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example2.json new file mode 100644 index 00000000..86cd683f --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example2.json @@ -0,0 +1,103 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "and", + "args": [ + { + "op": "=", + "args": [ + { + "property": "collection" + }, + "landsat8_l1tp" + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + }, + { + "op": ">=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2021-04-08T04:39:23Z" + } + ] + }, + { + "op": "s_intersects", + "args": [ + { + "property": "geometry" + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + 43.5845, + -79.5442 + ], + [ + 43.6079, + -79.4893 + ], + [ + 43.5677, + -79.4632 + ], + [ + 43.6129, + -79.3925 + ], + [ + 43.6223, + -79.3238 + ], + [ + 43.6576, + -79.3163 + ], + [ + 43.7945, + -79.1178 + ], + [ + 43.8144, + -79.1542 + ], + [ + 43.8555, + -79.1714 + ], + [ + 43.7509, + -79.6390 + ], + [ + 43.5845, + -79.5442 + ] + ] + ] + } + ] + } + ] + }, + "result": { + "{http://a9.com/-/opensearch/extensions/eo/1.0/}parentIdentifier": "landsat8_l1tp", + "{http://a9.com/-/opensearch/extensions/eo/1.0/}cloudCover": "10]", + "{http://a9.com/-/opensearch/extensions/time/1.0/}start": "2021-04-08T04:39:23Z", + "{http://a9.com/-/opensearch/extensions/geo/1.0/}relation": "intersects", + "{http://a9.com/-/opensearch/extensions/geo/1.0/}geometry": "POLYGON((43.5845 -79.5442,43.6079 -79.4893,43.5677 -79.4632,43.6129 -79.3925,43.6223 -79.3238,43.6576 -79.3163,43.7945 -79.1178,43.8144 -79.1542,43.8555 -79.1714,43.7509 -79.639,43.5845 -79.5442))" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example3.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example3.json new file mode 100644 index 00000000..b374ac7f --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example3.json @@ -0,0 +1,17 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "and", + "args": [ + { + "op": ">", + "args": [ { "property": "sentinel:data_coverage" }, 50 ] + }, + { + "op": "<", + "args": [ { "property": "eo:cloud_cover" }, 10 ] + } + ] + }, + "exception": "System.NotSupportedException" +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example4.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example4.json new file mode 100644 index 00000000..9fe2f76c --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example4.json @@ -0,0 +1,17 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "or", + "args": [ + { + "op": ">", + "args": [ { "property": "sentinel:data_coverage" }, 50 ] + }, + { + "op": "<", + "args": [ { "property": "eo:cloud_cover" }, 10 ] + } + ] + }, + "exception": "System.NotSupportedException" +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example5.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example5.json new file mode 100644 index 00000000..4526f1d3 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example5.json @@ -0,0 +1,11 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "=", + "args": [ + { "property": "prop1" }, + { "property": "prop2" } + ] + }, + "exception": "System.NotSupportedException" +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example6.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example6.json new file mode 100644 index 00000000..f77b6658 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example6.json @@ -0,0 +1,22 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "t_intersects", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-11-11T00:00:00Z", + "2020-11-12T00:00:00Z" + ] + } + ] + }, + "result": { + "{http://a9.com/-/opensearch/extensions/time/1.0/}relation": "intersects", + "{http://a9.com/-/opensearch/extensions/time/1.0/}start": "2020-11-11T00:00:00Z", + "{http://a9.com/-/opensearch/extensions/time/1.0/}end": "2020-11-12T00:00:00Z" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example7.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example7.json new file mode 100644 index 00000000..9529c482 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example7.json @@ -0,0 +1,42 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "s_intersects", + "args": [ + { + "property": "geometry" + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -77.0824, + 38.7886 + ], + [ + -77.0189, + 38.7886 + ], + [ + -77.0189, + 38.8351 + ], + [ + -77.0824, + 38.8351 + ], + [ + -77.0824, + 38.7886 + ] + ] + ] + } + ] + }, + "result": { + "{http://a9.com/-/opensearch/extensions/geo/1.0/}relation": "intersects", + "{http://a9.com/-/opensearch/extensions/geo/1.0/}geometry": "POLYGON((-77.0824 38.7886,-77.0189 38.7886,-77.0189 38.8351,-77.0824 38.8351,-77.0824 38.7886))" + } +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example8.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example8.json new file mode 100644 index 00000000..0b5d5ea8 --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example8.json @@ -0,0 +1,79 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "or", + "args": [ + { + "op": "s_intersects", + "args": [ + { + "property": "geometry" + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -77.0824, + 38.7886 + ], + [ + -77.0189, + 38.7886 + ], + [ + -77.0189, + 38.8351 + ], + [ + -77.0824, + 38.8351 + ], + [ + -77.0824, + 38.7886 + ] + ] + ] + } + ] + }, + { + "op": "s_intersects", + "args": [ + { + "property": "geometry" + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -79.0935, + 38.7886 + ], + [ + -79.0290, + 38.7886 + ], + [ + -79.0290, + 38.8351 + ], + [ + -79.0935, + 38.8351 + ], + [ + -79.0935, + 38.7886 + ] + ] + ] + } + ] + } + ] + }, + "exception": "System.NotSupportedException" +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example9.json b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example9.json new file mode 100644 index 00000000..be256d2f --- /dev/null +++ b/src/Stars.Data.Tests/Resources/Suppliers/Opensearchable/CQL2Tests_Example9.json @@ -0,0 +1,44 @@ +{ + "filter-lang": "cql2-json", + "filter": { + "op": "or", + "args": [ + { + "op": ">", + "args": [ + { + "property": "sentinel:data_coverage" + }, + 50 + ] + }, + { + "op": "<", + "args": [ + { + "property": "landsat:coverage_percent" + }, + 10 + ] + }, + { + "op": "and", + "args": [ + { + "op": "isNull", + "args": { + "property": "sentinel:data_coverage" + } + }, + { + "op": "isNull", + "args": { + "property": "landsat:coverage_percent" + } + } + ] + } + ] + }, + "exception": "System.NotSupportedException" +} \ No newline at end of file diff --git a/src/Stars.Data.Tests/Suppliers/SearchExpressionSupplierTests.cs b/src/Stars.Data.Tests/Suppliers/SearchExpressionSupplierTests.cs new file mode 100644 index 00000000..ee824746 --- /dev/null +++ b/src/Stars.Data.Tests/Suppliers/SearchExpressionSupplierTests.cs @@ -0,0 +1,89 @@ +using System.Linq; +using Newtonsoft.Json; +using Xunit; +using Stac.Extensions.Projection; +using Terradue.Stars.Data.Translators; +using Stac; +using Terradue.Stars.Services.Model.Stac; +using Terradue.Stars.Services.Model.Atom; +using System.Threading; +using System.Xml; +using Terradue.ServiceModel.Ogc.Owc.AtomEncoding; +using System; +using Terradue.Metadata.EarthObservation.OpenSearch.Extensions; +using Terradue.Metadata.EarthObservation.Ogc.Extensions; +using System.Collections.Generic; +using Terradue.Stars.Interface.Supplier; +using System.IO; +using Newtonsoft.Json.Linq; +using Stac.Api.Models.Cql2; +using Xunit.Abstractions; +using Stac.Api.Converters; +using Amazon.Runtime.Internal.Util; +using Terradue.Stars.Data.Suppliers; +using Microsoft.Extensions.DependencyInjection; +using Terradue.Stars.Services.Translator; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Google.Apis.Bigquery.v2.Data; +using Xunit.Sdk; + +namespace Terradue.Data.Tests.Suppliers +{ + public class SearchExpressionSupplierTests : TestBase + { + private readonly JsonSerializerSettings _settings; + + public SearchExpressionSupplierTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + _settings = new JsonSerializerSettings(); + _settings.Converters.Add(new BooleanExpressionConverter()); + + + } + + public static IEnumerable AllSuppliersTestsData + { + get + { + return new SuppliersTestsData(); + } + } + + [Theory, MemberData("AllSuppliersTestsData", DisableDiscoveryEnumeration = true)] + public async Task AllSuppliersSearchExpression(string key, ISupplier supplier, string file) + { + string json = File.ReadAllText(file); + JObject jObject = JObject.Parse(json); + var be = JsonConvert.DeserializeObject(jObject["filter"].ToString(), _settings); + CQL2Expression cql = new CQL2Expression(be); + + // Task result = null; + // get the result without the exception + + try + { + var task = await supplier.InternalSearchExpressionAsync(cql, CancellationToken.None); + if (jObject["result"] != null) + { + var resultJson = JsonConvert.SerializeObject(task); + JsonAssert.AreEqual(jObject["result"].ToString(), resultJson); + } + } + catch (Exception e) + { + if (jObject["exception"] != null) + { + Assert.Equal(Type.GetType(jObject["exception"].ToString()), e.GetType()); + } + else + { + throw new Exception($"{Path.GetFileName(file)}: {e.Message}", e); + } + } + + } + + } + +} diff --git a/src/Stars.Data.Tests/Suppliers/SuppliersTestsData.cs b/src/Stars.Data.Tests/Suppliers/SuppliersTestsData.cs new file mode 100644 index 00000000..b749a85a --- /dev/null +++ b/src/Stars.Data.Tests/Suppliers/SuppliersTestsData.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections; +using System.Collections.Generic; +using Terradue.Stars.Data.Model.Metadata; +using System.Reflection; +using Terradue.Stars.Interface.Processing; +using Xunit.Abstractions; +using System.IO; +using Microsoft.Extensions.Configuration; +using Terradue.Stars.Interface.Supplier; +using Terradue.Stars.Data.Suppliers; +using Microsoft.Extensions.Logging; +using Terradue.Stars.Services.Translator; + +namespace Terradue.Data.Tests.Suppliers +{ + public class SuppliersTestsData : TestBase, IEnumerable + { + public SuppliersTestsData() : base() + { + Collection.AddSingleton(sp => + { + var supplier = new OpenSearchableSupplier(sp.GetRequiredService>(), + sp.GetRequiredService()); + supplier.Key = "OpenSearchable"; + return supplier; + }); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IDictionary GetSuppliersResourcePaths() + { + return configuration.GetSection("Tests:Suppliers").Get>(); + } + + public IEnumerator GetEnumerator() + { + foreach (ISupplier supplier in ServiceProvider.GetService>()) + { + var paths = GetSuppliersResourcePaths(); + if (!paths.ContainsKey(supplier.Key)) + continue; + foreach (var path in paths[supplier.Key]) + { + foreach (var file in Directory.GetFiles(GetResourceFilePath("Suppliers/" + path), "*.json", new EnumerationOptions() { RecurseSubdirectories = true })) + { + yield return new object[] { supplier.Key, supplier, file }; + } + } + } + } + } +} diff --git a/src/Stars.Data.Tests/Terradue.Stars.Data.Tests.csproj b/src/Stars.Data.Tests/Terradue.Stars.Data.Tests.csproj index 5b67afb4..c3c62ac5 100644 --- a/src/Stars.Data.Tests/Terradue.Stars.Data.Tests.csproj +++ b/src/Stars.Data.Tests/Terradue.Stars.Data.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Stars.Data.Tests/TestBase.cs b/src/Stars.Data.Tests/TestBase.cs index 839ca83a..b295549a 100644 --- a/src/Stars.Data.Tests/TestBase.cs +++ b/src/Stars.Data.Tests/TestBase.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; +using System.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,12 +13,14 @@ using Stac.Schemas; using Terradue.Stars.Interface; using Terradue.Stars.Interface.Supplier; +using Terradue.Stars.Services.Credentials; using Terradue.Stars.Services.Model.Stac; using Terradue.Stars.Services.Plugins; using Terradue.Stars.Services.Resources; using Terradue.Stars.Services.Supplier.Carrier; using Terradue.Stars.Services.ThirdParty.Egms; using Terradue.Stars.Services.ThirdParty.Titiler; +using Terradue.Stars.Services.Translator; using Xunit; using Xunit.Abstractions; @@ -66,6 +69,8 @@ protected TestBase() Collection.AddSingleton(); Collection.AddSingleton(); Collection.AddSingleton(); + Collection.AddSingleton(); + Collection.AddSingleton(); Collection.AddHttpClient(); var builder = new ConfigurationBuilder(); // tell the builder to look for the appsettings.json file diff --git a/src/Stars.Data.Tests/testsettings.json b/src/Stars.Data.Tests/testsettings.json index 26b920af..fa092696 100644 --- a/src/Stars.Data.Tests/testsettings.json +++ b/src/Stars.Data.Tests/testsettings.json @@ -1,94 +1,99 @@ { - "Tests": { - "Harvesters": { - "KOMPSAT-3": [ - "KARI/KOMPSAT-3" - ], - "KOMPSAT-5": [ - "KARI/KOMPSAT-5" - ], - "SENTINEL-1": [ - "ESA/SENTINEL-1" - ], - "SENTINEL-2": [ - "ESA/SENTINEL-2" - ], - "ICEYE": [ - "ICEYE" - ], - "TERRASAR-X": [ - "DLR/TERRASAR-X" - ], - "GAOFEN": [ - "CNSA/GAOFEN-1", - "CNSA/GAOFEN-2", - "CNSA/GAOFEN-4" - ], - "GAOFEN-3": [ - "CNSA/GAOFEN-3" - ], - "LANDSAT8": [ - "NASA/LANDSAT8" - ], - "LANDSAT9": [ - "NASA/LANDSAT9" - ], - "DIMAP": [ - "DMC" - ], - "ALOS2": [ - "JAXA/ALOS2" - ], - "CONAE": [ - "CONAE/SAOCOM-1" - ], - "RCM": [ - "CSA/RCM-1", - "CSA/RCM-2", - "CSA/RCM-3" - ], - "AIRBUS": [ - "AIRBUS" - ], - "ABAE": [ - "ABAE" - ], - "WORLDVIEW": [ - "DIGITALGLOBE/WORLDVIEW" - ], - "GEOEYE": [ - "DIGITALGLOBE/GEOEYE" - ], - "INPE": [ - "INPE" - ], - "KANOPUS-V": [ - "ROSCOSMOS/KANOPUS-V" - ], - "BKA" : [ - "BKA" - ], - "ISRO": [ - "ISRO" - ], - "PLANET": [ - "PLANETSCOPE" - ], - "CSK": [ - "ASI/COSMO-SKYMED" - ], - "GDAL": [ - "GDAL" - ], - "RESURSP": [ - "ROSCOSMOS/RESURS-P" - ], - "BLACKSKY": [ - "BLACKSKY/Global" - ], - "NEWSAT": [ - "SATELLOGIC/NEWSAT" - ] + "Tests": { + "Harvesters": { + "KOMPSAT-3": [ + "KARI/KOMPSAT-3" + ], + "KOMPSAT-5": [ + "KARI/KOMPSAT-5" + ], + "SENTINEL-1": [ + "ESA/SENTINEL-1" + ], + "SENTINEL-2": [ + "ESA/SENTINEL-2" + ], + "ICEYE": [ + "ICEYE" + ], + "TERRASAR-X": [ + "DLR/TERRASAR-X" + ], + "GAOFEN": [ + "CNSA/GAOFEN-1", + "CNSA/GAOFEN-2", + "CNSA/GAOFEN-4" + ], + "GAOFEN-3": [ + "CNSA/GAOFEN-3" + ], + "LANDSAT8": [ + "NASA/LANDSAT8" + ], + "LANDSAT9": [ + "NASA/LANDSAT9" + ], + "DIMAP": [ + "DMC" + ], + "ALOS2": [ + "JAXA/ALOS2" + ], + "CONAE": [ + "CONAE/SAOCOM-1" + ], + "RCM": [ + "CSA/RCM-1", + "CSA/RCM-2", + "CSA/RCM-3" + ], + "AIRBUS": [ + "AIRBUS" + ], + "ABAE": [ + "ABAE" + ], + "WORLDVIEW": [ + "DIGITALGLOBE/WORLDVIEW" + ], + "GEOEYE": [ + "DIGITALGLOBE/GEOEYE" + ], + "INPE": [ + "INPE" + ], + "KANOPUS-V": [ + "ROSCOSMOS/KANOPUS-V" + ], + "BKA": [ + "BKA" + ], + "ISRO": [ + "ISRO" + ], + "PLANET": [ + "PLANETSCOPE" + ], + "CSK": [ + "ASI/COSMO-SKYMED" + ], + "GDAL": [ + "GDAL" + ], + "RESURSP": [ + "ROSCOSMOS/RESURS-P" + ], + "BLACKSKY": [ + "BLACKSKY/Global" + ], + "NEWSAT": [ + "SATELLOGIC/NEWSAT" + ] + }, + "Suppliers": { + "OpenSearchable": [ + "Opensearchable" + ] + } } - } } \ No newline at end of file diff --git a/src/Stars.Data/Routers/OpenSearchResultFeedRoutable.cs b/src/Stars.Data/Routers/OpenSearchResultFeedRoutable.cs new file mode 100644 index 00000000..53359087 --- /dev/null +++ b/src/Stars.Data/Routers/OpenSearchResultFeedRoutable.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Terradue.Stars.Services.Model.Atom; +using Terradue.OpenSearch.Result; +using Terradue.ServiceModel.Syndication; +using Terradue.Stars.Interface; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Collections; + +namespace Terradue.Stars.Data.Routers +{ + public class OpenSearchResultFeedRoutable : IItemCollection, IStreamResource + { + protected IOpenSearchResultCollection osCollection; + + private Uri sourceUri; + protected readonly ILogger logger; + + public OpenSearchResultFeedRoutable(IOpenSearchResultCollection collection, Uri sourceUri, ILogger logger) + { + this.osCollection = collection; + this.sourceUri = sourceUri; + this.logger = logger; + } + + public OpenSearchResultFeedRoutable() + { + } + + public IOpenSearchResultCollection OpenSearchResultCollection => osCollection; + + public string Label => osCollection.Title == null ? osCollection.Identifier : osCollection.Title.Text; + + public ContentType ContentType => new ContentType("application/atom+xml"); + + public Uri Uri => SelfLink == null ? sourceUri : SelfLink.Uri; + + public SyndicationLink SelfLink => osCollection.Links.FirstOrDefault(l => l.RelationshipType == "self"); + + public ResourceType ResourceType => ResourceType.Collection; + + public string Id => osCollection.Identifier; + + public string Filename => Id + ".atom.xml"; + + public ulong ContentLength => Convert.ToUInt64(Encoding.Default.GetBytes(ReadAsStringAsync(System.Threading.CancellationToken.None)).Length); + + public bool IsCatalog => false; + + public ContentDisposition ContentDisposition => new ContentDisposition() { FileName = Filename }; + + public bool CanBeRanged => false; + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => throw new NotImplementedException(); + + public string ReadAsStringAsync(CancellationToken ct) + { + StreamReader sr = new StreamReader(GetStreamAsync(ct).Result); + return sr.ReadToEnd(); + } + + public async Task GetStreamAsync(CancellationToken ct) + { + return await Task.Run(() => + { + AtomFeed atomFeed = AtomFeed.CreateFromOpenSearchResultCollection(osCollection) as AtomFeed; + MemoryStream ms = new MemoryStream(); + var sw = XmlWriter.Create(ms); + Atom10FeedFormatter atomFormatter = new Atom10FeedFormatter(atomFeed); + atomFormatter.WriteTo(sw); + sw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + return ms as Stream; + }); + } + + public Task GoToNode() + { + return Task.FromResult((IResource)this); + } + + public Task GetStreamAsync(long start, CancellationToken ct, long end = -1) + { + throw new NotImplementedException(); + } + + public IReadOnlyList GetLinks() + { + return osCollection.Links.Select(l => new AtomResourceLink(l)).ToList(); + } + + public void Add(IItem item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(IItem item) + { + throw new NotImplementedException(); + } + + public void CopyTo(IItem[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public bool Remove(IItem item) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + return osCollection.Items.Select(i => new AtomItemNode(AtomItem.FromOpenSearchResultItem(i), new Uri(Uri, i.Id))).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return osCollection.Items.Select(i => new AtomItemNode(AtomItem.FromOpenSearchResultItem(i), new Uri(Uri, i.Id))).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Stars.Data/Suppliers/Astrium/GeodeliverySupplier.cs b/src/Stars.Data/Suppliers/Astrium/GeodeliverySupplier.cs index ea3f2ae8..be3a275b 100644 --- a/src/Stars.Data/Suppliers/Astrium/GeodeliverySupplier.cs +++ b/src/Stars.Data/Suppliers/Astrium/GeodeliverySupplier.cs @@ -15,6 +15,7 @@ using Terradue.Stars.Services.Translator; using System.Linq; using Stac.Extensions.File; +using Stac.Api.Interfaces; namespace Terradue.Stars.Data.Suppliers.Astrium { @@ -145,5 +146,17 @@ public virtual Task Order(IOrderable orderableRoute) { throw new NotSupportedException(); } + + public Task SearchForAsync(ISearchExpression searchExpression, CancellationToken ct) + { + // the GeodeliverySupplier supplier will never return a resource from a search expression + logger.LogWarning("The GeodeliverySupplier supplier cannot search for resource from a search expression"); + return null; + } + + public Task InternalSearchExpressionAsync(ISearchExpression searchExpression, CancellationToken ct) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/Stars.Data/Suppliers/OpenSearchableSupplier.cs b/src/Stars.Data/Suppliers/OpenSearchableSupplier.cs index d05dcc06..52a2a7bb 100644 --- a/src/Stars.Data/Suppliers/OpenSearchableSupplier.cs +++ b/src/Stars.Data/Suppliers/OpenSearchableSupplier.cs @@ -14,6 +14,11 @@ using System.Text.RegularExpressions; using Stac; using System.Threading; +using Stac.Api.Interfaces; +using Stac.Api.Models.Cql2; +using Itenso.TimePeriod; +using Terradue.GeoJson.Geometry; +using Terradue.Stars.Geometry.Wkt; namespace Terradue.Stars.Data.Suppliers { @@ -24,7 +29,10 @@ public class OpenSearchableSupplier : ISupplier protected ILogger logger; protected OpenSearchEngine opensearchEngine; - public OpenSearchableSupplier(ILogger logger, TranslatorManager translatorManager) + public OpenSearchableSupplier(ILogger logger, TranslatorManager translatorManager) : this(logger as ILogger, translatorManager) + { } + + internal OpenSearchableSupplier(ILogger logger, TranslatorManager translatorManager) { this.opensearchEngine = new OpenSearchEngine(); this.opensearchEngine.RegisterExtension(new Terradue.OpenSearch.Engine.Extensions.AtomOpenSearchEngineExtension()); @@ -43,7 +51,7 @@ public OpenSearchableSupplier(ILogger logger, TranslatorManager translatorManage public virtual async Task SearchForAsync(IResource node, CancellationToken ct, string identifierRegex = null) { - return (IResource)new OpenSearchResultItemRoutable((await QueryAsync(node, ct, identifierRegex)).Items.FirstOrDefault(), new Uri("os://" + openSearchable.Identifier), logger); + return (IResource)new OpenSearchResultItemRoutable((await QueryAsync(node, ct, identifierRegex)).Items.FirstOrDefault(), new Uri("os://opensearch"), logger); } public virtual async Task QueryAsync(IResource node, CancellationToken ct, string identifierRegex = null) @@ -134,5 +142,467 @@ public virtual Task Order(IOrderable orderableRoute) { throw new NotSupportedException(); } + + public async Task SearchForAsync(ISearchExpression searchExpression, CancellationToken ct) + { + NameValueCollection nvc = CreateOpenSearchParametersFromExpression(searchExpression); + if (nvc == null) return null; + logger.LogDebug("OpenSearch parameters: {0}", string.Join(",", nvc.AllKeys.Select(k => $"{k}={nvc[k]}"))); + + AtomFeed atomFeed = await Task.Run(() => (AtomFeed)opensearchEngine.Query(openSearchable, nvc, typeof(AtomFeed))); + + return new OpenSearchResultFeedRoutable(atomFeed, new Uri("os://opensearch"), logger); + } + + private NameValueCollection CreateOpenSearchParametersFromExpression(ISearchExpression searchExpression) + { + // Here stands the big piece of work to translate the search expression to OpenSearch parameters + + // First, we only support CQL2SearchExpression for now + if (!(searchExpression is CQL2Expression)) + { + logger.LogWarning("The OpenSearchableSupplier supplier cannot search for resource from a search expression other than CQL2"); + return null; + } + + // Basically, Opensearch shall supports only the first level of search expression + CQL2Expression cql2Expression = searchExpression as CQL2Expression; + + // Prepare an empty collection of parameters + NameValueCollection parameters = new NameValueCollection(); + int level = 0; + + // Let's iterate over the search expression + FillParametersFromBooleanExpression(cql2Expression.Expression, parameters, level); + + return parameters; + + } + + private void FillParametersFromBooleanExpression(BooleanExpression booleanExpression, NameValueCollection parameters, int level) + { + switch (booleanExpression) + { + case AndOrExpression andOrExpression: + // we do not support OR operator + if (andOrExpression.Op == AndOrExpressionOp.Or) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with OR operator"); + // we simply recurse on the operands + foreach (var arg in andOrExpression.Args) + { + FillParametersFromBooleanExpression(arg, parameters, level + 1); + } + return; + case NotExpression notExpression: + // we do not support NOT operator + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with NOT operator"); + case SpatialPredicate spatialPredicate: + FillParametersFromSpatialPredicate(spatialPredicate, parameters, level + 1); + return; + case TemporalPredicate temporalPredicate: + FillParametersFromTemporalPredicate(temporalPredicate, parameters, level + 1); + return; + case ComparisonPredicate comparisonPredicate: + FillParametersFromComparisonPredicate(comparisonPredicate, parameters, level + 1); + return; + case ArrayPredicate arrayPredicate: + FillParametersFromArrayPredicate(arrayPredicate, parameters, level + 1); + return; + } + } + + private void FillParametersFromArrayPredicate(ArrayPredicate arrayPredicate, NameValueCollection parameters, int v) + { + throw new NotImplementedException(); + } + + private void FillParametersFromTemporalPredicate(TemporalPredicate temporalPredicate, NameValueCollection parameters, int v) + { + // We support only the comparison between a property and a literal + // So we extract the property and the literal + string propertyName = null; + Itenso.TimePeriod.ITimeInterval timeInterval = null; + foreach (var arg in temporalPredicate.Args) + { + if (arg is PropertyRef propertyRef) + { + propertyName = propertyRef.Property; + } + else if (arg is ITemporalLiteral temporalLiteral) + { + timeInterval = TemporalLiteralToTimeInterval(temporalLiteral); + } + } + if (propertyName == null || timeInterval == null) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with temporal predicate with other than property and literal"); + // check the property name + switch (propertyName) + { + case "datetime": + FillStartStopParametersFromTimeInterval(timeInterval, temporalPredicate.Op, parameters); + return; + case "updated": + // only works with the after operator + if (temporalPredicate.Op != TemporalPredicateOp.T_after) + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with temporal predicate on '{propertyName}' property with other than after operator"); + parameters.Set("{http://purl.org/dc/terms/}modified", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with temporal predicate on '{propertyName}' property"); + } + } + + private void FillStartStopParametersFromTimeInterval(ITimeInterval timeInterval, TemporalPredicateOp op, NameValueCollection parameters) + { + switch (op) + { + case TemporalPredicateOp.T_after: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}start", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + return; + case TemporalPredicateOp.T_before: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}end", timeInterval.End.ToString("yyyy-MM-ddTHH:mm:ssZ")); + return; + case TemporalPredicateOp.T_during: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}start", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}end", timeInterval.End.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}relation", "during"); + return; + case TemporalPredicateOp.T_equals: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}start", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}end", timeInterval.End.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}relation", "equals"); + return; + case TemporalPredicateOp.T_disjoint: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}start", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}end", timeInterval.End.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}relation", "disjoint"); + return; + case TemporalPredicateOp.T_contains: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}start", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}end", timeInterval.End.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}relation", "contains"); + return; + default: + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}start", timeInterval.Start.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}end", timeInterval.End.ToString("yyyy-MM-ddTHH:mm:ssZ")); + parameters.Set("{http://a9.com/-/opensearch/extensions/time/1.0/}relation", "intersects"); + return; + } + } + + private ITimeInterval TemporalLiteralToTimeInterval(ITemporalLiteral temporalLiteral) + { + if (temporalLiteral is InstantLiteral timeInstantLiteral) + { + return new TimeInterval(timeInstantLiteral.DateTime.DateTime, timeInstantLiteral.DateTime.DateTime); + } + else if (temporalLiteral is IntervalLiteral timePeriodLiteral) + { + return timePeriodLiteral.TimeInterval; + } + else + { + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with temporal literal other than instant or period"); + } + } + + private void FillParametersFromSpatialPredicate(SpatialPredicate spatialPredicate, NameValueCollection parameters, int v) + { + // We support only the comparison between a property and a literal + // So we extract the property and the literal + string propertyName = null; + ISpatialLiteral spatialLiteral = null; + foreach (var arg in spatialPredicate.Args) + { + if (arg is PropertyRef propertyRef) + { + propertyName = propertyRef.Property; + } + else if (arg is ISpatialLiteral spatialLiteralArg) + { + spatialLiteral = spatialLiteralArg; + } + } + if (propertyName == null || spatialLiteral == null) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with spatial predicate with other than property and literal"); + // check the property name + switch (propertyName) + { + case "geometry": + FillParametersFromGeometryLiteral(spatialLiteral, spatialPredicate.Op, parameters); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with spatial predicate on '{propertyName}' property"); + } + } + + private void FillParametersFromGeometryLiteral(ISpatialLiteral spatialLiteral, SpatialPredicateOp op, NameValueCollection parameters) + { + switch (spatialLiteral) + { + case GeometryLiteral geometryLiteral: + switch (op) + { + case SpatialPredicateOp.S_intersects: + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}geometry", geometryLiteral.GeometryObject.ToWkt()); + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}relation", "intersects"); + return; + case SpatialPredicateOp.S_contains: + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}geometry", geometryLiteral.GeometryObject.ToWkt()); + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}relation", "contains"); + return; + case SpatialPredicateOp.S_disjoint: + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}geometry", geometryLiteral.GeometryObject.ToWkt()); + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}relation", "disjoint"); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with spatial predicate with '{op}' operator"); + } + case EnvelopeLiteral envelopeLiteral: + switch (op) + { + case SpatialPredicateOp.S_intersects: + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}box", envelopeLiteral.ToString()); + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}relation", "intersects"); + return; + case SpatialPredicateOp.S_contains: + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}box", envelopeLiteral.ToString()); + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}relation", "contains"); + return; + case SpatialPredicateOp.S_disjoint: + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}box", envelopeLiteral.ToString()); + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}relation", "disjoint"); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with spatial predicate with '{op}' operator"); + } + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with spatial predicate with other than geometry literal"); + } + } + + private void FillParametersFromComparisonPredicate(ComparisonPredicate comparisonPredicate, NameValueCollection parameters, int v) + { + switch (comparisonPredicate) + { + case BinaryComparisonPredicate binaryComparisonPredicate: + FillParametersFromBinaryComparisonPredicate(binaryComparisonPredicate, parameters, v); + return; + case IsLikePredicate isLikePredicate: + FillParametersFromIsLikePredicate(isLikePredicate, parameters, v); + return; + case IsInListPredicate isInListPredicate: + FillParametersFromIsInListPredicate(isInListPredicate, parameters, v); + return; + case IsBetweenPredicate isBetweenPredicate: + FillParametersFromIsBetweenPredicate(isBetweenPredicate, parameters, v); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with comparison predicate with other than binary comparison, is like or is in list"); + } + } + + private void FillParametersFromIsBetweenPredicate(IsBetweenPredicate isBetweenPredicate, NameValueCollection parameters, int v) + { + // We support only the comparison between a property and a literal + // So we extract the property and the literal + string propertyName = null; + string min = null; + string max = null; + foreach (var arg in isBetweenPredicate.Args) + { + if (arg is PropertyRef propertyRef) + { + propertyName = propertyRef.Property; + } + else if (arg is IScalarExpression scalarExpression) + { + if (min == null) + min = scalarExpression.ToString(); + else + max = scalarExpression.ToString(); + } + } + if (propertyName == null || min == null || max == null) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with is between predicate with other than property and literal"); + // check the property name + switch (propertyName) + { + case "collection": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}parentIdentifier", $"[{min},{max}]"); + return; + case "keywords": + parameters.Set("{http://purl.org/dc/terms/}subject", $"[{min},{max}]"); + return; + case "eo:cloud_cover": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}cloudCover", $"[{min},{max}]"); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with is in list predicate on '{propertyName}' property"); + } + } + + private void FillParametersFromIsInListPredicate(IsInListPredicate isInListPredicate, NameValueCollection parameters, int v) + { + // We support only the comparison between a property and a literal + // So we extract the property and the literal + string propertyName = null; + string[] values = null; + foreach (var arg in isInListPredicate.Args) + { + if (arg is PropertyRef propertyRef) + { + propertyName = propertyRef.Property; + } + else if (arg is ScalarExpressionCollection scalarExpressions) + { + values = scalarExpressions.Select(se => se.ToString()).ToArray(); + } + } + if (propertyName == null || values == null) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with is in list predicate with other than property and literal"); + // check the property name + switch (propertyName) + { + case "collection": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}parentIdentifier", "{" + string.Join(",", values) + "}"); + return; + case "keywords": + parameters.Set("{http://purl.org/dc/terms/}subject", "{" + string.Join(",", values) + "}"); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with is in list predicate on '{propertyName}' property"); + } + } + + private void FillParametersFromIsLikePredicate(IsLikePredicate isLikePredicate, NameValueCollection parameters, int v) + { + // We support only the comparison between a property and a literal + // So we extract the property and the literal + string propertyName = null; + string value = null; + foreach (var arg in isLikePredicate.Args) + { + if (arg is PropertyRef propertyRef) + { + propertyName = propertyRef.Property; + } + else if (arg is IScalarExpression scalarExpression) + { + value = scalarExpression.ToString(); + } + } + if (propertyName == null || value == null) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with is like predicate with other than property and literal"); + // check the property name + switch (propertyName) + { + case "id": + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}uid", value); + return; + case "collection": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}parentIdentifier", value); + return; + case "keywords": + parameters.Set("{http://purl.org/dc/terms/}subject", value); + return; + case "eo:cloud_cover": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}cloudCover", value); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with is like predicate on '{propertyName}' property"); + } + } + + private void FillParametersFromBinaryComparisonPredicate(BinaryComparisonPredicate binaryComparisonPredicate, NameValueCollection parameters, int v) + { + // We support only the comparison between a property and a literal + // So we extract the property and the literal + string propertyName = null; + IScalarExpression value = null; + foreach (var arg in binaryComparisonPredicate.Args) + { + if (arg is PropertyRef propertyRef) + { + propertyName = propertyRef.Property; + } + else if (arg is IScalarExpression scalarExpression) + { + value = scalarExpression; + } + } + if (propertyName == null || value == null) + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with comparison predicate with other than property and literal"); + // check the property name + switch (propertyName) + { + case "id": + parameters.Set("{http://a9.com/-/opensearch/extensions/geo/1.0/}uid", ValueToNumberSetOrInterval(value.ToString(), binaryComparisonPredicate.Op)); + return; + case "mission": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}platform", ValueToNumberSetOrInterval(value.ToString(), binaryComparisonPredicate.Op)); + return; + case "datetime": + InstantLiteral instantLiteral = value as InstantLiteral; + if (instantLiteral == null) + { + throw new NotSupportedException("The OpenSearchableSupplier supplier cannot search for resource from a search expression with comparison predicate on datetime property with other than instant literal"); + } + FillStartStopParametersFromTimeInterval(TemporalLiteralToTimeInterval(value as ITemporalLiteral), BinaryComparisonPredicateToTemporalPredicateOp(binaryComparisonPredicate.Op), parameters); + return; + case "collection": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}parentIdentifier", ValueToNumberSetOrInterval(value.ToString(), binaryComparisonPredicate.Op)); + return; + case "eo:cloud_cover": + parameters.Set("{http://a9.com/-/opensearch/extensions/eo/1.0/}cloudCover", ValueToNumberSetOrInterval(value.ToString(), binaryComparisonPredicate.Op)); + return; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with is like predicate on '{propertyName}' property"); + } + } + + private TemporalPredicateOp BinaryComparisonPredicateToTemporalPredicateOp(ComparisonPredicateOp op) + { + switch (op) + { + case ComparisonPredicateOp.Eq: + return TemporalPredicateOp.T_equals; + case ComparisonPredicateOp.Gt: + case ComparisonPredicateOp.Ge: + return TemporalPredicateOp.T_after; + case ComparisonPredicateOp.Lt: + case ComparisonPredicateOp.Le: + return TemporalPredicateOp.T_before; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with comparison predicate with '{op}' operator"); + } + } + + private string ValueToNumberSetOrInterval(string value, ComparisonPredicateOp op) + { + switch (op) + { + case ComparisonPredicateOp.Eq: + return value; + case ComparisonPredicateOp.Gt: + return "]" + value; + case ComparisonPredicateOp.Ge: + return "[" + value; + case ComparisonPredicateOp.Lt: + return value + "["; + case ComparisonPredicateOp.Le: + return value + "]"; + default: + throw new NotSupportedException($"The OpenSearchableSupplier supplier cannot search for resource from a search expression with comparison predicate with '{op}' operator"); + } + } + + public Task InternalSearchExpressionAsync(ISearchExpression searchExpression, CancellationToken ct) + { + NameValueCollection nvc = CreateOpenSearchParametersFromExpression(searchExpression); + if (nvc == null) return null; + // return a dictionary + return Task.FromResult(nvc.AllKeys.ToDictionary(k => k, k => nvc[k])); + } } } \ No newline at end of file diff --git a/src/Stars.Data/Suppliers/PlanetScope/PlanetScopeSupplier.cs b/src/Stars.Data/Suppliers/PlanetScope/PlanetScopeSupplier.cs index 120b99e5..9738666f 100644 --- a/src/Stars.Data/Suppliers/PlanetScope/PlanetScopeSupplier.cs +++ b/src/Stars.Data/Suppliers/PlanetScope/PlanetScopeSupplier.cs @@ -18,6 +18,7 @@ using Terradue.Stars.Services.Supplier; using Terradue.Stars.Services.Translator; using Stac.Extensions.File; +using Stac.Api.Interfaces; namespace Terradue.Stars.Data.Suppliers.PlanetScope { @@ -176,6 +177,16 @@ public virtual Task Order(IOrderable orderableRoute) { throw new NotSupportedException(); } + + public Task SearchForAsync(ISearchExpression searchExpression, CancellationToken ct) + { + throw new NotImplementedException(); + } + + public Task InternalSearchExpressionAsync(ISearchExpression searchExpression, CancellationToken ct) + { + throw new NotImplementedException(); + } } diff --git a/src/Stars.Geometry/Terradue.Stars.Geometry.csproj b/src/Stars.Geometry/Terradue.Stars.Geometry.csproj index 8569e13d..96c4ef96 100644 --- a/src/Stars.Geometry/Terradue.Stars.Geometry.csproj +++ b/src/Stars.Geometry/Terradue.Stars.Geometry.csproj @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/src/Stars.Interface/IItemCollection.cs b/src/Stars.Interface/IItemCollection.cs new file mode 100644 index 00000000..7df3e996 --- /dev/null +++ b/src/Stars.Interface/IItemCollection.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Net; +using System.Net.Mime; +using System.Threading.Tasks; +using Stac; +using Terradue.Stars.Interface.Router; + +namespace Terradue.Stars.Interface +{ + public interface IItemCollection : ICollection + { + IReadOnlyList GetLinks(); + } +} \ No newline at end of file diff --git a/src/Stars.Interface/Supplier/ISupplier.cs b/src/Stars.Interface/Supplier/ISupplier.cs index 87f2cf0c..3c9ddac2 100644 --- a/src/Stars.Interface/Supplier/ISupplier.cs +++ b/src/Stars.Interface/Supplier/ISupplier.cs @@ -1,3 +1,4 @@ +using Stac.Api.Interfaces; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -13,6 +14,10 @@ public interface ISupplier : IPlugin Task SearchForAsync(IResource item, CancellationToken ct, string identifierRegex = null); + Task SearchForAsync(ISearchExpression searchExpression, CancellationToken ct); + + Task InternalSearchExpressionAsync(ISearchExpression searchExpression, CancellationToken ct); + Task Order(IOrderable orderableRoute); } } \ No newline at end of file diff --git a/src/Stars.Interface/Terradue.Stars.Interface.csproj b/src/Stars.Interface/Terradue.Stars.Interface.csproj index 321c9537..fe3c029a 100644 --- a/src/Stars.Interface/Terradue.Stars.Interface.csproj +++ b/src/Stars.Interface/Terradue.Stars.Interface.csproj @@ -9,9 +9,11 @@ - + + + diff --git a/src/Stars.Services/Supplier/NativeSupplier.cs b/src/Stars.Services/Supplier/NativeSupplier.cs index 4fb65f52..9ad2472e 100644 --- a/src/Stars.Services/Supplier/NativeSupplier.cs +++ b/src/Stars.Services/Supplier/NativeSupplier.cs @@ -8,6 +8,7 @@ using Terradue.Stars.Interface; using Terradue.Stars.Services.Plugins; using System.Threading; +using Stac.Api.Interfaces; namespace Terradue.Stars.Services.Supplier { @@ -37,5 +38,16 @@ public Task Order(IOrderable orderableRoute) { throw new NotSupportedException(); } + + public Task SearchForAsync(ISearchExpression searchExpression, CancellationToken ct) + { + // the Native supplier will never return a resource from a search expression + return null; + } + + public Task InternalSearchExpressionAsync(ISearchExpression searchExpression, CancellationToken ct) + { + throw new NotImplementedException(); + } } } diff --git a/src/Stars.Services/Terradue.Stars.Services.csproj b/src/Stars.Services/Terradue.Stars.Services.csproj index 52875fc5..d12e8930 100644 --- a/src/Stars.Services/Terradue.Stars.Services.csproj +++ b/src/Stars.Services/Terradue.Stars.Services.csproj @@ -33,7 +33,8 @@ - + + @@ -51,6 +52,7 @@ +