Skip to content

Commit

Permalink
Merge pull request #5 from CBenghi/feature/file-extensions
Browse files Browse the repository at this point in the history
Improved support for serilialising and deserialising Specification Groups in Xml
  • Loading branch information
CBenghi authored Aug 30, 2023
2 parents 61a3b7e + 69101f3 commit b5bdfa9
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 15 deletions.
84 changes: 83 additions & 1 deletion Xbim.InformationSpecifications.NewTests/IoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Xbim.InformationSpecifications.Tests.Helpers;
using Xunit;
Expand Down Expand Up @@ -143,7 +144,7 @@ public void CanLoadOptionalFacet()
}


[Fact]
[Fact]
public void CanLoadXml()
{
var f = new FileInfo(@"Files/IDS_example-with-restrictions.xml");
Expand Down Expand Up @@ -188,5 +189,86 @@ public void NoWarnsOnCurrentJsonVersion()
LoggingTestHelper.NoIssues(loggerMock);
File.Delete(filename);
}

[Fact]
public void CanSaveXmlAsZip()
{
Xids? x = BuildMultiSpecGroupIDS();

using var ms = new MemoryStream();

x.ExportBuildingSmartIDS(ms);

// Check the stream is a PK Zip stream by looking at 'magic' first 4 bytes
Xids.IsZipped(ms).Should().BeTrue();
}

[Fact]
public void ZippedIDSSpecsContainsIDS()
{
Xids? x = BuildMultiSpecGroupIDS();

using var ms = new MemoryStream();

x.ExportBuildingSmartIDS(ms);

// Check Contains IDS files & content
using var archive = new ZipArchive(ms, ZipArchiveMode.Read, false);

archive.Entries.Should().HaveCount(2);

archive.Entries.Should().AllSatisfy(e => e.Name.Should().EndWith(".ids", "IDS file extension expected"));
archive.Entries.Should().AllSatisfy(e => e.Length.Should().BeGreaterThan(0, "Content expected"));
}

[Fact]
public void CanLoadXmlAsZip()
{
Xids? x = BuildMultiSpecGroupIDS();

using var ms = new MemoryStream();

x.ExportBuildingSmartIDS(ms);
ms.Position = 0;

var newIds = Xids.LoadBuildingSmartIDS(ms);

newIds.Should().NotBeNull();
newIds!.SpecificationsGroups.Should().HaveCount(2);
}

[Fact]
public void CanLoadXmlFromZipFile()
{
Xids? x = BuildMultiSpecGroupIDS();
var tempXmlFile = Path.ChangeExtension(Path.GetTempFileName(), "zip");
using (var fs = new FileStream(tempXmlFile, FileMode.Create))
{
x.ExportBuildingSmartIDS(fs);
}

var newIds = Xids.LoadBuildingSmartIDS(tempXmlFile);

newIds.Should().NotBeNull();
newIds!.SpecificationsGroups.Should().HaveCount(2);
}

private static Xids BuildMultiSpecGroupIDS()
{
var file = new FileInfo(@"bsFiles/bsFilesSelf/TestFile.ids");
var x = Xids.Load(file);
x.Should().NotBeNull();
x!.AllSpecifications().Should().HaveCount(1);

// Add a 2nd group with a basic spec
var specGroup = new SpecificationsGroup(x);
x.SpecificationsGroups.Add(specGroup);

var newSpec = x.PrepareSpecification(specGroup, IfcSchemaVersion.Undefined);
newSpec.Applicability.Facets.Add(new IfcTypeFacet() { IfcType = "Door" });

x!.AllSpecifications().Should().HaveCount(2);
return x;
}
}
}
12 changes: 12 additions & 0 deletions Xbim.InformationSpecifications/Helpers/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;

namespace Xbim.InformationSpecifications.Helpers
Expand Down Expand Up @@ -26,5 +27,16 @@ public static string FirstCharToUpper(this string input) =>
_ => input.First().ToString().ToUpper() + input[1..]
#endif
};

private static char[] InvalidChars = Path.GetInvalidFileNameChars();
/// <summary>
/// Makes a filename safe by escaping reserved characters
/// </summary>
/// <param name="filename"></param>
/// <returns>a Safe filename</returns>
public static string MakeSafeFileName(this string filename)
{
return InvalidChars.Aggregate(filename, (current, c) => current.Replace(c, '_'));
}
}
}
30 changes: 30 additions & 0 deletions Xbim.InformationSpecifications/IO/Xids.IO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ public static bool CanLoad(FileInfo sourceFile, ILogger? logger = null)
return false;
}

const int ZipMagic = 0x04034b50; // Zip magic number: PK/003/004

/// <summary>
/// Determines if the stream is zipped
/// </summary>
/// <param name="stream"></param>
/// <param name="logger"></param>
/// <returns></returns>
public static bool IsZipped(Stream stream, ILogger? logger = null)
{
if(!stream.CanSeek)
{
return false;
}
try
{
stream.Position = 0;
var bytes = new byte[4];
stream.Read(bytes, 0, 4);
var magic = BitConverter.ToInt32(bytes, 0);
stream.Position = 0;

return magic == ZipMagic;
}
catch
{
return false;
}
}

/// <summary>
/// Loads a xids model from any of the suppoerted files.
/// </summary>
Expand Down
79 changes: 65 additions & 14 deletions Xbim.InformationSpecifications/IO/Xids.Io.xml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml;
using System.Xml.Linq;
using Xbim.InformationSpecifications.Cardinality;
using Xbim.InformationSpecifications.Facets.buildingSMART;
using Xbim.InformationSpecifications.Helpers;

namespace Xbim.InformationSpecifications
{
Expand Down Expand Up @@ -75,9 +75,10 @@ public ExportedFormat ExportBuildingSmartIDS(Stream destinationStream, ILogger?
int i = 0;
foreach (var specGroup in SpecificationsGroups)
{
var name = (specGroup.Name is not null && !string.IsNullOrEmpty(specGroup.Name) && specGroup.Name.IndexOfAny(Path.GetInvalidFileNameChars()) < 0)
? $"{++i} - {specGroup.Name}.xml"
: $"{++i}.xml";

var name = (string.IsNullOrEmpty(specGroup.Name))
? $"{++i}.ids"
: $"{++i} - {specGroup.Name!.MakeSafeFileName()}.ids";
var file = zipArchive.CreateEntry(name);
using var str = file.Open();
using XmlWriter writer = XmlWriter.Create(str, WriteSettings);
Expand Down Expand Up @@ -442,15 +443,48 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter)


/// <summary>
/// Attempts to unpersist an XIDS from a stream.
/// Attempts to load an XIDS from a stream, where the stream is either an XML IDS or a zip file containing multiple IDS XML files
/// </summary>
/// <param name="stream">The XML source stream to parse.</param>
/// <param name="stream">The XML or ZIP source stream to parse.</param>
/// <param name="logger">The logger to send any errors and warnings to.</param>
/// <returns>an XIDS or null if it could not be read.</returns>
public static Xids? LoadBuildingSmartIDS(Stream stream, ILogger? logger = null)
{
var t = XElement.Load(stream);
return LoadBuildingSmartIDS(t, logger);
if (IsZipped(stream))
{
using(var zip = new ZipArchive(stream, ZipArchiveMode.Read, false))
{
var xids = new Xids();
foreach(var entry in zip.Entries)
{
try
{
if(entry.Name.EndsWith(".ids", StringComparison.InvariantCultureIgnoreCase))
{
using(var idsStream = entry.Open())
{
var element = XElement.Load(idsStream);
LoadBuildingSmartIDS(element, logger, xids);
}
}
}
catch(Exception ex)
{
logger?.LogError(ex, "Failed to load IDS file from zip stream");
}
}
if(!xids.AllSpecifications().Any())
{
logger?.LogWarning("No specifications found in this zip file. Ensure the zip contains *.ids files");
}
return xids;
}
}
else
{
var t = XElement.Load(stream);
return LoadBuildingSmartIDS(t, logger);
}
}

/// <summary>
Expand All @@ -463,7 +497,7 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter)
}

/// <summary>
/// Attempts to unpersist an XIDS from a file, given the file name.
/// Attempts to unpersist an XIDS from the provider IDS XML file or zip file containing IDS files.
/// </summary>
/// <param name="fileName">File name of the Xids to load</param>
/// <param name="logger">The logger to send any errors and warnings to.</param>
Expand All @@ -476,12 +510,28 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter)
logger?.LogError("File '{fileName}' not found from executing directory '{fullDirectoryName}'", fileName, d.FullName);
return null;
}
var main = XElement.Parse(File.ReadAllText(fileName));
return LoadBuildingSmartIDS(main, logger);
if(fileName.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
{
using var stream = File.OpenRead(fileName);
if(IsZipped(stream))
{
return LoadBuildingSmartIDS(stream, logger);
}
else
{
logger?.LogError("Not a valid zip file");
return null;
}
}
else
{
var main = XElement.Parse(File.ReadAllText(fileName));
return LoadBuildingSmartIDS(main, logger);
}
}

/// <summary>
/// Should use <see cref="LoadBuildingSmartIDS(XElement, ILogger?)"/> instead.
/// Should use <see cref="LoadBuildingSmartIDS(XElement, ILogger?, Xids?)"/> instead.
/// </summary>
[Obsolete("Use LoadBuildingSmartIDS instead.")]
public static Xids? ImportBuildingSmartIDS(XElement main, ILogger? logger = null)
Expand All @@ -494,12 +544,13 @@ static private void WriteFacetBaseElements(FacetBase cf, XmlWriter xmlWriter)
/// </summary>
/// <param name="main">the IDS element to load.</param>
/// <param name="logger">the logging context</param>
/// <param name="ids"></param>
/// <returns>an entire new XIDS of null on errors</returns>
public static Xids? LoadBuildingSmartIDS(XElement main, ILogger? logger = null)
public static Xids? LoadBuildingSmartIDS(XElement main, ILogger? logger = null, Xids? ids = null)
{
if (main.Name.LocalName == "ids")
{
var ret = new Xids();
var ret = ids ?? new Xids();
var grp = new SpecificationsGroup(ret);
ret.SpecificationsGroups.Add(grp);
foreach (var sub in main.Elements())
Expand Down

0 comments on commit b5bdfa9

Please sign in to comment.