Skip to content

Commit

Permalink
REST I/F implementation of getting all types and a specific type decl…
Browse files Browse the repository at this point in the history
…ared in a nodeset, including type information in Json Schema format.
  • Loading branch information
barnstee committed Mar 3, 2025
1 parent 19e900b commit a59357c
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 17 deletions.
86 changes: 76 additions & 10 deletions UACloudLibraryServer/CloudLibDataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CESMII.OpcUa.NodeSetModel;
using CESMII.OpcUa.NodeSetModel.Opc.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Logging;
using NpgsqlTypes;
using Opc.Ua.Cloud.Library.DbContextModels;
using Opc.Ua.Cloud.Library.Interfaces;
using Opc.Ua.Cloud.Library.Models;
Expand Down Expand Up @@ -171,11 +172,13 @@ private IQueryable<T> GetNodeModels<T>(Expression<Func<CloudLibNodeSetModel, IEn
{
nodeSets = _dbContext.nodeSets.AsQueryable();
}

IQueryable<T> nodeModels = nodeSets.SelectMany(selector);
if (!string.IsNullOrEmpty(nodeId))
{
nodeModels = nodeModels.Where(ot => ot.NodeId == nodeId);
}

return nodeModels;
}

Expand Down Expand Up @@ -395,19 +398,19 @@ public UANodesetResult[] FindNodesets(string[] keywords, int? offset, int? limit
}


public string[] GetAllNamespacesAndNodesets()
public Task<string[]> GetAllNamespacesAndNodesets()
{
try
{
string[] namesAndIds = _dbContext.nodeSets.Select(nsm => new { nsm.ModelUri, nsm.Identifier, nsm.Version, nsm.PublicationDate }).Select(n => $"{n.ModelUri},{n.Identifier},{n.Version},{n.PublicationDate}").ToArray();
return namesAndIds;
return Task.FromResult(namesAndIds);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}

return Array.Empty<string>();
return Task.FromResult(Array.Empty<string>());
}

private CloudLibNodeSetModel GetNamespaceUriForNodeset(string nodesetId)
Expand All @@ -425,20 +428,20 @@ private CloudLibNodeSetModel GetNamespaceUriForNodeset(string nodesetId)
return null;
}

public string[] GetAllNamesAndNodesets()
public Task<string[]> GetAllNamesAndNodesets()
{
try
{
var categoryAndNodesetIds = _dbContext.NamespaceMetaData.Select(md => new { md.Category.Name, md.NodesetId }).ToList();
string[] namesAndNodesetsString = categoryAndNodesetIds.Select(cn => $"{cn.Name},{cn.NodesetId}").ToArray();
return namesAndNodesetsString;
return Task.FromResult(namesAndNodesetsString);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}

return Array.Empty<string>();
return Task.FromResult(Array.Empty<string>());
}


Expand Down Expand Up @@ -657,12 +660,75 @@ private void MapToOrganisation(Organisation org, OrganisationModel model)
org.LogoUrl = model.LogoUrl != null ? new Uri(model.LogoUrl) : null;
}

public async Task<string[]> GetAllTypes(string nodeSetID)
{
CloudLibNodeSetModel nodeSetMeta = await GetNodeSets(nodeSetID).FirstOrDefaultAsync().ConfigureAwait(false);
if (nodeSetMeta != null)
{
List<NodeModel> types = new();
types.AddRange(GetObjectTypes(nodeSetMeta.ModelUri));
types.AddRange(GetVariableTypes(nodeSetMeta.ModelUri));
types.AddRange(GetDataTypes(nodeSetMeta.ModelUri));
types.AddRange(GetReferenceTypes(nodeSetMeta.ModelUri));

List<string> typeList = new();
foreach(NodeModel model in types)
{
typeList.Add(model.BrowseName + ";" + model.NodeIdIdentifier);
}

return typeList.ToArray();
}

return Array.Empty<string>();
}

public Task<string> GetUAType(string expandedNodeId)
{
// create a substring from expandedNodeId by removing "nsu=" from the start and parsing until the first ";"
string modelUri = expandedNodeId.Substring(4, expandedNodeId.IndexOf(';', StringComparison.OrdinalIgnoreCase) - 4);

JsonSerializerOptions options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
MaxDepth = 100,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
ReferenceHandler = ReferenceHandler.IgnoreCycles
};

ObjectTypeModel objectModel = GetNodeModels<ObjectTypeModel>(nsm => nsm.ObjectTypes, modelUri, null, expandedNodeId).FirstOrDefault();
if (objectModel != null)
{
return Task.FromResult("\"JsonSchema\": " + options.GetJsonSchemaAsNode(typeof(UAObjectType)).ToString() + ",\r\n\"Value\": \"" + objectModel.BrowseName + "\"");
}

VariableTypeModel variableModel = GetNodeModels<VariableTypeModel>(nsm => nsm.VariableTypes, modelUri, null, expandedNodeId).FirstOrDefault();
if (variableModel != null)
{
return Task.FromResult("\"JsonSchema\": " + options.GetJsonSchemaAsNode(typeof(UAVariableType)).ToString() + ",\r\n\"Value\": \"" + variableModel.BrowseName + "\"");
}

DataTypeModel dataModel = GetNodeModels<DataTypeModel>(nsm => nsm.DataTypes, modelUri, null, expandedNodeId).FirstOrDefault();
if (dataModel != null)
{
string fields = JsonSerializer.Serialize(dataModel.StructureFields, options);
return Task.FromResult("\"JsonSchema\": " + options.GetJsonSchemaAsNode(typeof(UADataType)).ToString() + ",\r\n\"Value\": \"" + dataModel.BrowseName + "\",\r\n\"Structure Fields\": " + fields);
}

ReferenceTypeModel referenceModel = GetNodeModels<ReferenceTypeModel>(nsm => nsm.ReferenceTypes, modelUri, null, expandedNodeId).FirstOrDefault();
if (referenceModel != null)
{
return Task.FromResult("\"JsonSchema\": " + options.GetJsonSchemaAsNode(typeof(UAReferenceType)).ToString() + ",\r\n\"Value\": \"" + referenceModel.BrowseName + "\"");
}

return Task.FromResult(string.Empty);
}

#if !NOLEGACY
#region legacy

public IQueryable<MetadataModel> GetMetadataModel()
{
// TODO retrieve well-known properties from NamespaceMetaDataMoel
// TODO retrieve well-known properties from NamespaceMetaDataModel
return _dbContext.Metadata.AsQueryable();
}

Expand Down
51 changes: 49 additions & 2 deletions UACloudLibraryServer/Controllers/InfoModelController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
using Opc.Ua.Cloud.Library.Models;
using Opc.Ua.Export;
using Swashbuckle.AspNetCore.Annotations;
using static HotChocolate.ErrorCodes;

namespace Opc.Ua.Cloud.Library.Controllers
{
Expand Down Expand Up @@ -83,7 +84,7 @@ public IActionResult FindNamespaceAsync(
[SwaggerResponse(statusCode: 200, type: typeof(string[]), description: "All OPC UA Information Model namespace URIs and associated identifiers of the models found in the UA Cloud Library.")]
public IActionResult GetAllNamespacesandIdentifiersAsync()
{
string[] results = _database.GetAllNamespacesAndNodesets();
string[] results = _database.GetAllNamespacesAndNodesets().GetAwaiter().GetResult();
return new ObjectResult(results) { StatusCode = (int)HttpStatusCode.OK };
}

Expand All @@ -92,10 +93,56 @@ public IActionResult GetAllNamespacesandIdentifiersAsync()
[SwaggerResponse(statusCode: 200, type: typeof(string[]), description: "All OPC UA Information Model names and associated identifiers of the models found in the UA Cloud Library.")]
public IActionResult GetAllNamesandIdentifiersAsync()
{
string[] results = _database.GetAllNamesAndNodesets();
string[] results = _database.GetAllNamesAndNodesets().GetAwaiter().GetResult();
return new ObjectResult(results) { StatusCode = (int)HttpStatusCode.OK };
}

[HttpGet]
[Route("/infomodel/types/{identifier}")]
[SwaggerResponse(statusCode: 200, type: typeof(string[]), description: "The OPC UA Information model types.")]
[SwaggerResponse(statusCode: 400, type: typeof(string), description: "The identifier provided could not be parsed.")]
[SwaggerResponse(statusCode: 404, type: typeof(string), description: "The identifier provided could not be found.")]
public async Task<IActionResult> GetAllTypesAsync(
[FromRoute][Required][SwaggerParameter("OPC UA Information model identifier.")] string identifier
)
{
if (!uint.TryParse(identifier, out uint nodeSetID))
{
return new ObjectResult("Could not parse identifier") { StatusCode = (int)HttpStatusCode.BadRequest };
}

string[] types = await _database.GetAllTypes(identifier).ConfigureAwait(false);
if ((types == null) || (types.Length == 0))
{
return new ObjectResult("Failed to find nodeset metadata") { StatusCode = (int)HttpStatusCode.NotFound };
}

return new ObjectResult(types) { StatusCode = (int)HttpStatusCode.OK };
}

[HttpGet]
[Route("/infomodel/type")]
[SwaggerResponse(statusCode: 200, type: typeof(string), description: "The OPC UA type and its metadata.")]
[SwaggerResponse(statusCode: 400, type: typeof(string), description: "The expended node ID provided could not be parsed.")]
[SwaggerResponse(statusCode: 404, type: typeof(string), description: "The expended node ID provided could not be found.")]
public async Task<IActionResult> GetTypeAsync(
[FromQuery][SwaggerParameter("The expanded node ID of the type requested, starting with nsu=.")] string expandedNodeId
)
{
if (string.IsNullOrEmpty(expandedNodeId) || !expandedNodeId.StartsWith("nsu=", StringComparison.InvariantCulture))
{
return new ObjectResult("Could not parse expanded node ID") { StatusCode = (int)HttpStatusCode.BadRequest };
}

string typeInfo = await _database.GetUAType(expandedNodeId).ConfigureAwait(false);
if (string.IsNullOrEmpty(typeInfo))
{
return new ObjectResult("Failed to find type information") { StatusCode = (int)HttpStatusCode.NotFound };
}

return new ObjectResult(typeInfo) { StatusCode = (int)HttpStatusCode.OK };
}

[HttpGet]
[Route("/infomodel/download/{identifier}")]
[SwaggerResponse(statusCode: 200, type: typeof(UANameSpace), description: "The OPC UA Information model and its metadata.")]
Expand Down
20 changes: 18 additions & 2 deletions UACloudLibraryServer/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,44 @@ public interface IDatabase

Task<UANameSpace> RetrieveAllMetadataAsync(uint nodesetId);

string[] GetAllNamespacesAndNodesets();
Task<string[]> GetAllNamespacesAndNodesets();

string[] GetAllNamesAndNodesets();
Task<string[]> GetAllNamesAndNodesets();

Task<string[]> GetAllTypes(string nodeSetID);

Task<string> GetUAType(string expandedNodeId);

IQueryable<ObjectTypeModel> GetObjectTypes(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<VariableTypeModel> GetVariableTypes(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<DataTypeModel> GetDataTypes(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<PropertyModel> GetProperties(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<DataVariableModel> GetDataVariables(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<ReferenceTypeModel> GetReferenceTypes(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<InterfaceModel> GetInterfaces(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<ObjectModel> GetObjects(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<MethodModel> GetMethods(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<NodeModel> GetAllNodes(string modelUri = null, DateTime? publicationDate = null, string nodeId = null);

IQueryable<CategoryModel> GetCategories();

IQueryable<OrganisationModel> GetOrganisations();

IQueryable<NamespaceMetaDataModel> GetNamespaces();

int GetNamespaceTotalCount();

Task<NamespaceMetaDataModel> ApproveNamespaceAsync(string identifier, ApprovalStatus status, string approvalInformation, List<UAProperty> additionalProperties);

IQueryable<CloudLibNodeSetModel> GetNodeSetsPendingApproval();

#if !NOLEGACY
Expand Down
7 changes: 4 additions & 3 deletions UACloudLibraryServer/Models/UANameSpace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ public UANameSpace()
NumberOfDownloads = 0;
Nodeset = new Nodeset();
}

[Required]
public Nodeset Nodeset { get; set; }
/// <summary>
/// Time the nodeset was submitted to the cloud library
/// </summary>

public DateTime? CreationTime { get; set; }

public uint NumberOfDownloads { get; set; }
}

Expand Down Expand Up @@ -234,6 +234,7 @@ public Nodeset()
public DateTime PublicationDate { get; set; }

public DateTime LastModifiedDate { get; set; }

public string ValidationStatus { get; set; }

public List<CloudLibRequiredModelInfo> RequiredModels { get; set; }
Expand Down
7 changes: 7 additions & 0 deletions UACloudLibraryServer/NodeSetIndex/CloudLibNodeSetModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ namespace Opc.Ua.Cloud.Library
public class CloudLibNodeSetModel : NodeSetModel
{
public virtual NamespaceMetaDataModel Metadata { get; set; }

public ValidationStatus ValidationStatus { get; set; }

public string ValidationStatusInfo { get; set; }

public TimeSpan ValidationElapsedTime { get; set; }

public DateTime? ValidationFinishedTime { get; set; }

public string[] ValidationErrors { get; set; }

public DateTime? LastModifiedDate { get; set; }

internal static async Task<CloudLibNodeSetModel> FromModelAsync(ModelTableEntry model, AppDbContext dbContext)
{
var nodeSetModel = new CloudLibNodeSetModel();
Expand Down

0 comments on commit a59357c

Please sign in to comment.