From 0626ecf1ad6725db61236aaafd94a6ce4a5e78ae Mon Sep 17 00:00:00 2001 From: EvAlex Date: Thu, 18 Jan 2018 18:17:39 +0300 Subject: [PATCH 1/3] fix(mpd) parse xml (#2) --- DashTools.Samples/Program.cs | 18 +++++++++++++----- DashTools/MediaPresentationDescription.cs | 1 - 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/DashTools.Samples/Program.cs b/DashTools.Samples/Program.cs index 05ae761..2f18a7f 100644 --- a/DashTools.Samples/Program.cs +++ b/DashTools.Samples/Program.cs @@ -11,6 +11,15 @@ namespace Qoollo.MpegDash.Samples { class Program { + private static readonly string[] mpdFiles = new string[] + { + "http://ncplusgo.s43-po.live.e56-po.insyscd.net/out/u/eskatvsd.mpd", + "http://dash.edgesuite.net/envivio/EnvivioDash3/manifest.mpd", + "http://10.5.5.7/q/p/userapi/streams/32/mpd", + "http://10.5.7.207/userapi/streams/30/mpd", + "http://10.5.7.207/userapi/streams/11/mpd?start_time=1458816642&stop_time=1458819642", + }; + static void Main(string[] args) { Task.Run(async () => @@ -22,10 +31,9 @@ static void Main(string[] args) static async Task MainAsync(string[] args) { string dir = "envivio"; - string mpdUrl = "http://10.5.5.7/q/p/userapi/streams/32/mpd"; - //"http://10.5.7.207/userapi/streams/30/mpd"; - //"http://10.5.7.207/userapi/streams/11/mpd?start_time=1458816642&stop_time=1458819642"; - //"http://dash.edgesuite.net/envivio/EnvivioDash3/manifest.mpd"; + string mpdUrl = mpdFiles[0]; + var from = TimeSpan.Zero; + var to = TimeSpan.MaxValue; var stopwatch = Stopwatch.StartNew(); var downloader = new MpdDownloader(new Uri(mpdUrl), dir); @@ -38,7 +46,7 @@ static async Task MainAsync(string[] args) // ? new Mp4InitFile(Path.Combine("envivio", Path.GetFileName(f))) // : new Mp4File(Path.Combine("envivio", Path.GetFileName(f)))) // .ToArray(); - await downloader.Download(trackRepresentation, TimeSpan.FromMinutes(60), TimeSpan.FromMinutes(60 + 60 * 6 / 6)); + await downloader.Download(trackRepresentation, from, to); var downloadTime = stopwatch.Elapsed - prepareTime; var ffmpeg = new FFMpegConverter(); diff --git a/DashTools/MediaPresentationDescription.cs b/DashTools/MediaPresentationDescription.cs index 70de511..35a0241 100644 --- a/DashTools/MediaPresentationDescription.cs +++ b/DashTools/MediaPresentationDescription.cs @@ -140,7 +140,6 @@ private XElement ReadMpdTag() { using (var reader = XmlReader.Create(stream)) { - stream.Seek(0, SeekOrigin.Begin); reader.ReadToFollowing("MPD"); return XNode.ReadFrom(reader) as XElement; } From 1027c367d197bbd94997d10ab92c737ba6a94adb Mon Sep 17 00:00:00 2001 From: EvAlex Date: Thu, 18 Jan 2018 18:20:32 +0300 Subject: [PATCH 2/3] implement segment timeline (#3) --- DashTools/DashTools.csproj | 5 ++ DashTools/Mpd/AssetIdentifier.cs | 16 ++++ DashTools/Mpd/BaseUrl.cs | 21 +++++ DashTools/Mpd/DescriptorType.cs | 55 +++++++++++++ DashTools/MpdAdaptationSet.cs | 7 ++ DashTools/MpdDownloader.cs | 57 +++++++++---- DashTools/MpdElement.cs | 5 +- DashTools/MpdPeriod.cs | 115 ++++++++++++++++++++++++++- DashTools/MpdSegmentTemplate.cs | 29 ++++++- DashTools/SegmentTimeline.cs | 36 +++++++++ DashTools/SegmentTimelineItem.cs | 68 ++++++++++++++++ DashTools/TrackRepresentation.cs | 61 +++++++++++--- DashTools/XmlAttributeParseHelper.cs | 32 +++++++- 13 files changed, 475 insertions(+), 32 deletions(-) create mode 100644 DashTools/Mpd/AssetIdentifier.cs create mode 100644 DashTools/Mpd/BaseUrl.cs create mode 100644 DashTools/Mpd/DescriptorType.cs create mode 100644 DashTools/SegmentTimeline.cs create mode 100644 DashTools/SegmentTimelineItem.cs diff --git a/DashTools/DashTools.csproj b/DashTools/DashTools.csproj index e77fcdb..440af9a 100644 --- a/DashTools/DashTools.csproj +++ b/DashTools/DashTools.csproj @@ -40,6 +40,7 @@ + @@ -54,10 +55,14 @@ + + + + diff --git a/DashTools/Mpd/AssetIdentifier.cs b/DashTools/Mpd/AssetIdentifier.cs new file mode 100644 index 0000000..e9b0375 --- /dev/null +++ b/DashTools/Mpd/AssetIdentifier.cs @@ -0,0 +1,16 @@ +using System.Xml.Linq; + +namespace Qoollo.MpegDash.Mpd +{ + /// + /// The AssetIdentifier is used to identify the asset on Period level. If two different Periods contain + /// equivalent Asset Identifiers then the content in the two Periods belong to the same asset. + /// + public class AssetIdentifier : DescriptorType + { + public AssetIdentifier(XElement node) + : base(node) + { + } + } +} \ No newline at end of file diff --git a/DashTools/Mpd/BaseUrl.cs b/DashTools/Mpd/BaseUrl.cs new file mode 100644 index 0000000..858f2c4 --- /dev/null +++ b/DashTools/Mpd/BaseUrl.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Qoollo.MpegDash.Mpd +{ + public class BaseUrl : MpdElement + { + public BaseUrl(XElement node) + : base(node) + { + } + + public string Value + { + get { return node.Value; } + } + } +} diff --git a/DashTools/Mpd/DescriptorType.cs b/DashTools/Mpd/DescriptorType.cs new file mode 100644 index 0000000..5f1b3d8 --- /dev/null +++ b/DashTools/Mpd/DescriptorType.cs @@ -0,0 +1,55 @@ +using System; +using System.Xml.Linq; + +namespace Qoollo.MpegDash.Mpd +{ + public class DescriptorType : MpdElement + { + public DescriptorType(XElement node) + : base(node) + { + } + + /// + /// Mandatory. + /// + /// Specifies a URI to identify the scheme. The semantics of this + /// element are specific to the scheme specified by this attribute. + /// The @schemeIdUri may be a URN or URL.When a URL is + /// used, it should also contain a month-date in the form + /// mmyyyy; the assignment of the URL must have been + /// authorized by the owner of the domain name in that URL on + /// or very close to that date, to avoid problems when domain + /// names change ownership. + /// + public string SchemeIdUri + { + get { return helper.ParseMandatoryString("schemeIdUri"); } + } + + /// + /// Optional + /// + /// Specifies the value for the descriptor element. The value + /// space and semantics must be defined by the owners of the + /// scheme identified in the @schemeIdUri attribute. + /// + public string Value + { + get { return helper.ParseOptionalString("value"); } + } + + /// + /// Optional + /// + /// specifies an identifier for the descriptor. Descriptors with + /// identical values for this attribute shall be synonymous, i.e. + /// the processing of one of the descriptors with an identical + /// value is sufficient. + /// + public string Id + { + get { return helper.ParseOptionalString("id"); } + } + } +} \ No newline at end of file diff --git a/DashTools/MpdAdaptationSet.cs b/DashTools/MpdAdaptationSet.cs index 0a530f0..cf7368d 100644 --- a/DashTools/MpdAdaptationSet.cs +++ b/DashTools/MpdAdaptationSet.cs @@ -107,6 +107,13 @@ public uint SubsegmentStartsWithSAP } } + /// + /// Specifies default Segment Template information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentTemplate and + /// Representation.SegmentTemplate, if present. + /// public MpdSegmentTemplate SegmentTemplate { get { return segmentTemplate.Value; } diff --git a/DashTools/MpdDownloader.cs b/DashTools/MpdDownloader.cs index ed4b3cf..7bdc2d9 100644 --- a/DashTools/MpdDownloader.cs +++ b/DashTools/MpdDownloader.cs @@ -9,7 +9,7 @@ namespace Qoollo.MpegDash { - public class MpdDownloader + public class MpdDownloader : IDisposable { private readonly Uri mpdUrl; @@ -369,23 +369,19 @@ private Task DownloadFragment(string fragmentUrl) ? new Uri(fragmentUrl) : new Uri(mpdUrl, fragmentUrl); - string destPath = Path.Combine(destinationDir, GetLastPartOfPath(fragmentUrl)); - if (string.IsNullOrWhiteSpace(Path.GetExtension(destPath))) - destPath = Path.ChangeExtension(destPath, "mp4"); - if (File.Exists(destPath)) - destPath = Path.Combine(Path.GetDirectoryName(destPath), Path.ChangeExtension((Path.GetFileNameWithoutExtension(destPath) + "_1"), Path.GetExtension(destPath))); + string destPath = Path.Combine(destinationDir, GetFileNameForFragmentUrl(fragmentUrl)); + + int i = 0; + while (File.Exists(destPath)) + { + i++; + destPath = Path.Combine(Path.GetDirectoryName(destPath), Path.ChangeExtension((Path.GetFileNameWithoutExtension(destPath) + "_" + i), Path.GetExtension(destPath))); + } return Task.Factory.StartNew(() => { - try - { - client.DownloadFile(url, destPath); - return destPath; - } - catch - { - return null; - } + client.DownloadFile(url, destPath); + return destPath; }); } } @@ -420,7 +416,7 @@ private MpdWalker CreateMpdWalker() return new MpdWalker(mpd.Value); } - private string GetLastPartOfPath(string url) + private string GetFileNameForFragmentUrl(string url) { string fileName = url; if (IsAbsoluteUrl(url)) @@ -429,13 +425,40 @@ private string GetLastPartOfPath(string url) if (fileName.Contains("/")) fileName = fileName.Substring(fileName.LastIndexOf("/") + 1); } + + int queryStartIndex = fileName.IndexOf("?"); + if (queryStartIndex >= 0) + fileName = fileName.Substring(0, queryStartIndex); + + string extension = Path.GetExtension(fileName); + if (string.IsNullOrWhiteSpace(extension)) + fileName = Path.ChangeExtension(fileName, "mp4"); + + fileName = ReplaceIllegalCharsInFileName(fileName); + + return fileName; + } + + private string ReplaceIllegalCharsInFileName(string fileName) + { + var illegalChars = new[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|' }; + foreach (var ch in illegalChars) + { + fileName = fileName.Replace(ch, '_'); + } return fileName; } - bool IsAbsoluteUrl(string url) + private bool IsAbsoluteUrl(string url) { Uri result; return Uri.TryCreate(url, UriKind.Absolute, out result); } + + public void Dispose() + { + if (mpd.IsValueCreated) + mpd.Value.Dispose(); + } } } diff --git a/DashTools/MpdElement.cs b/DashTools/MpdElement.cs index 4f931ff..ae9da30 100644 --- a/DashTools/MpdElement.cs +++ b/DashTools/MpdElement.cs @@ -1,4 +1,7 @@ -using System.Xml.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; namespace Qoollo.MpegDash { diff --git a/DashTools/MpdPeriod.cs b/DashTools/MpdPeriod.cs index a846812..af6485f 100644 --- a/DashTools/MpdPeriod.cs +++ b/DashTools/MpdPeriod.cs @@ -1,4 +1,5 @@ -using System; +using Qoollo.MpegDash.Mpd; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -11,7 +12,13 @@ public class MpdPeriod : MpdElement internal MpdPeriod(XElement node) : base(node) { - this.adaptationSets = new Lazy>(ParseAdaptationSets); + baseUrls = new Lazy>(ParseBaseUrls); + segmentBase = new Lazy(ParseSegmentBase); + segmentList = new Lazy(ParseSegmentList); + segmentTemplate = new Lazy(ParseSegmentTemplate); + assetIdentifier = new Lazy(ParseAssetIdentifier); + adaptationSets = new Lazy>(ParseAdaptationSets); + } public string Id @@ -34,6 +41,110 @@ public bool BitstreamSwitching get { return helper.ParseOptionalBool("bitstreamSwitching", false); } } + /// + /// 0...N + /// + /// Specifies a base URL that can be used for reference resolution + /// and alternative URL selection + /// + public IEnumerable BaseUrls + { + get { return baseUrls.Value; } + } + private readonly Lazy> baseUrls; + + private IEnumerable ParseBaseUrls() + { + return node.Elements() + .Where(n => n.Name.LocalName == "BaseUrl") + .Select(n => new BaseUrl(n)); + } + + /// + /// 0...1 + /// + /// Specifies default Segment Base information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentBase and Representation.SegmentBase, if present. + /// + public SegmentBase SegmentBase + { + get { return segmentBase.Value; } + } + private readonly Lazy segmentBase; + + private SegmentBase ParseSegmentBase() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentBase") + .Select(n => new SegmentBase(n)) + .FirstOrDefault(); + } + + /// + /// 0...1 + /// + /// Specifies default Segment List information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentList and Representation.SegmentList, if present. + /// + public MpdSegmentList SegmentList + { + get { return segmentList.Value; } + } + private readonly Lazy segmentList; + + private MpdSegmentList ParseSegmentList() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentList") + .Select(n => new MpdSegmentList(n)) + .FirstOrDefault(); + } + + /// + /// 0...1 + /// + /// Specifies default Segment Template information. + /// + /// Information in this element is overridden by information in + /// AdapationSet.SegmentTemplate and Representation.SegmentTemplate, if present. + /// + public MpdSegmentTemplate SegmentTemplate + { + get { return segmentTemplate.Value; } + } + private readonly Lazy segmentTemplate; + + private MpdSegmentTemplate ParseSegmentTemplate() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentTemplate") + .Select(n => new MpdSegmentTemplate(n)) + .FirstOrDefault(); + } + + /// + /// 0...1 + /// + /// Specifies that this Period belongs to a certain asset. + /// + public AssetIdentifier AssetIdentifier + { + get { return assetIdentifier.Value; } + } + private readonly Lazy assetIdentifier; + + private AssetIdentifier ParseAssetIdentifier() + { + return node.Elements() + .Where(n => n.Name.LocalName == "AssetIdentifier") + .Select(n => new AssetIdentifier(n)) + .FirstOrDefault(); + } + public IEnumerable AdaptationSets { get { return adaptationSets.Value; } diff --git a/DashTools/MpdSegmentTemplate.cs b/DashTools/MpdSegmentTemplate.cs index 84b517d..a5ac000 100644 --- a/DashTools/MpdSegmentTemplate.cs +++ b/DashTools/MpdSegmentTemplate.cs @@ -1,18 +1,23 @@ -using System.Xml.Linq; +using System; +using System.Linq; +using System.Xml.Linq; namespace Qoollo.MpegDash { /// - /// Specifies Segment template information. + /// Specifies Segment Template information. /// public class MpdSegmentTemplate : MultipleSegmentBase { internal MpdSegmentTemplate(XElement node) : base(node) { + segmentTimeline = ParseSegmentTimeline(); } /// + /// Optional + /// /// Specifies the template to create the Media Segment List. /// public string Media @@ -21,6 +26,8 @@ public string Media } /// + /// Optional + /// /// Specifies the template to create the Index Segment List. /// If neither the $Number$ nor the $Time$ identifier is included, /// this provides the URL to a Representation Index. @@ -31,6 +38,8 @@ public string Index } /// + /// Optional + /// /// Specifies the template to create the Initialization Segment. /// Neither $Number$ nor the $Time$ identifier shall be included. /// @@ -40,6 +49,8 @@ public string Initialization } /// + /// Optional + /// /// Specifies the template to create the Bitstream Switching Segment. /// Neither $Number$ nor the $Time$ identifier shall be included. /// @@ -47,5 +58,19 @@ public bool BitstreamSwitching { get { return helper.ParseOptionalBool("bitstreamSwitching", false); } } + + public SegmentTimeline SegmentTimeline + { + get { return segmentTimeline; } + } + private readonly SegmentTimeline segmentTimeline; + + private SegmentTimeline ParseSegmentTimeline() + { + return node.Elements() + .Where(n => n.Name.LocalName == "SegmentTimeline") + .Select(n => new SegmentTimeline(n)) + .FirstOrDefault(); + } } } \ No newline at end of file diff --git a/DashTools/SegmentTimeline.cs b/DashTools/SegmentTimeline.cs new file mode 100644 index 0000000..3034dba --- /dev/null +++ b/DashTools/SegmentTimeline.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Qoollo.MpegDash +{ + public class SegmentTimeline : MpdElement, IEnumerable + { + private readonly IEnumerable items; + + public SegmentTimeline(XElement node) + : base(node) + { + items = ParseItems(); + } + + private IEnumerable ParseItems() + { + return node.Elements() + .Where(n => n.Name.LocalName == "S") + .Select(n => new SegmentTimelineItem(n)); + } + + public IEnumerator GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/DashTools/SegmentTimelineItem.cs b/DashTools/SegmentTimelineItem.cs new file mode 100644 index 0000000..e4ed131 --- /dev/null +++ b/DashTools/SegmentTimelineItem.cs @@ -0,0 +1,68 @@ +using System.Xml.Linq; + +namespace Qoollo.MpegDash +{ + public class SegmentTimelineItem : MpdElement + { + public SegmentTimelineItem(XElement node) + : base(node) + { + } + + /// + /// Optional + /// + /// Specifies the MPD start time, in @timescale units, the + /// first Segment in the series starts relative to the beginning + /// of the Period. + /// + /// The value of this attribute must be equal to or greater + /// than the sum of the previous S element earliest + /// presentation time and the sum of the contiguous + /// Segment durations. + /// + /// If the value of the attribute is greater than what is + /// expressed by the previous S element, it expresses + /// discontinuities in the timeline. + /// + /// If not present then the value shall be assumed to + /// be zero for the first S element and for the subsequent S + /// elements, the value shall be assumed to be the sum of + /// the previous S element's earliest presentation time and + /// contiguous duration (i.e.previous S@t + @d* (@r + 1)). + /// + public ulong? Time + { + get { return helper.ParseOptionalUlong("t"); } + } + + /// + /// Mandatory + /// + /// Specifies the Segment duration, in units of the value of + /// the @timescale. + /// + public ulong Duration + { + get { return helper.ParseMandatoryUlong("d"); } + } + + /// + /// Optional. Default: 0 + /// + /// Specifies the repeat count of the number of following + /// contiguous Segments with the same duration expressed + /// by the value of @d.This value is zero-based (e.g.a value + /// of three means four Segments in the contiguous series). + /// A negative value of the @r attribute of the S + /// element indicates that the duration indicated in @d + /// attribute repeats until the start of the next S + /// element, the end of the Period or until the next + /// MPD update. + /// + public int RepeatCount + { + get { return helper.ParseOptionalInt("r", 0).Value; } + } + } +} \ No newline at end of file diff --git a/DashTools/TrackRepresentation.cs b/DashTools/TrackRepresentation.cs index d077812..e4e11f6 100644 --- a/DashTools/TrackRepresentation.cs +++ b/DashTools/TrackRepresentation.cs @@ -81,18 +81,25 @@ private IEnumerable GetSegments() var segmentTemplate = adaptationSet.SegmentTemplate ?? representation.SegmentTemplate; if (segmentTemplate != null) { - int i = 1; - while (true) + var segments = GetSegmentsFromTimeline(segmentTemplate); + + bool hasTimelineItems = false; + foreach (var segment in segments) { - yield return new TrackRepresentationSegment + hasTimelineItems = true; + + yield return segment; + } + + if (!hasTimelineItems) + { + segments = GetSegmentsFromRepresentation(representation); + foreach (var segment in segments) { - Path = segmentTemplate.Media - .Replace("$RepresentationID$", representation.Id) - .Replace("$Number$", i.ToString()), - Duration = TimeSpan.FromMilliseconds(segmentTemplate.Duration.Value) - }; - i++; + yield return segment; + } } + } else if (representation.SegmentList != null) { @@ -109,6 +116,42 @@ private IEnumerable GetSegments() throw new Exception("Failed to determine Segments"); } + private IEnumerable GetSegmentsFromRepresentation(MpdRepresentation representation) + { + int i = 1; + while (true) + { + yield return new TrackRepresentationSegment + { + Path = representation.SegmentTemplate.Media + .Replace("$RepresentationID$", representation.Id) + .Replace("$Number$", i.ToString()), + Duration = TimeSpan.FromMilliseconds(representation.SegmentTemplate.Duration.Value) + }; + i++; + } + } + + private IEnumerable GetSegmentsFromTimeline(MpdSegmentTemplate segmentTemplate) + { + int i = 1; + foreach (var item in segmentTemplate.SegmentTimeline) + { + int count = Math.Max(1, item.RepeatCount); + for (int j = 0; j < count; j++) + { + yield return new TrackRepresentationSegment + { + Path = segmentTemplate.Media + .Replace("$RepresentationID$", representation.Id) + .Replace("$Number$", i.ToString()), + Duration = TimeSpan.FromMilliseconds(item.Duration) + }; + i++; + } + } + } + internal IEnumerable GetFragmentsPaths(TimeSpan from, TimeSpan to) { var span = TimeSpan.Zero; diff --git a/DashTools/XmlAttributeParseHelper.cs b/DashTools/XmlAttributeParseHelper.cs index cb0d9f6..19d7a6a 100644 --- a/DashTools/XmlAttributeParseHelper.cs +++ b/DashTools/XmlAttributeParseHelper.cs @@ -16,6 +16,20 @@ public XmlAttributeParseHelper(XElement node) this.node = node; } + public string ParseMandatoryString(string attributeName) + { + var attr = node.Attribute(attributeName); + if (attr == null) + throw new Exception("Attribute \"" + attributeName + "\" not found on element " + node.ToString()); + return attr.Value; + } + + public string ParseOptionalString(string attributeName) + { + var attr = node.Attribute(attributeName); + return attr == null ? null : attr.Value; + } + public DateTimeOffset? ParseDateTimeOffset(string attributeName, bool mandatoryCondition) { if (!mandatoryCondition && node.Attribute(attributeName) == null) @@ -25,7 +39,7 @@ public XmlAttributeParseHelper(XElement node) public uint ParseUint(string attributeName) { - return uint.Parse(node.Attribute("bandwidth").Value); + return uint.Parse(node.Attribute(attributeName).Value); } public DateTimeOffset? ParseOptionalDateTimeOffset(string attributeName, DateTimeOffset? defaultValue = null) @@ -60,6 +74,22 @@ public bool ParseOptionalBool(string attributeName, bool defaultValue) : uint.Parse(attr.Value); } + public int? ParseOptionalInt(string attributeName, int? defaultValue = null) + { + var attr = node.Attribute(attributeName); + return attr == null + ? defaultValue + : int.Parse(attr.Value); + } + + public ulong ParseMandatoryUlong(string attributeName) + { + var attr = node.Attribute(attributeName); + if (attr == null) + throw new Exception("Attribute \"" + attributeName + "\" not found on element " + node.ToString()); + return ulong.Parse(attr.Value); + } + public ulong? ParseOptionalUlong(string attributeName, ulong? defaultValue = null) { var attr = node.Attribute(attributeName); From c8320eae16fef5acaf7ab789917d98ab2164520a Mon Sep 17 00:00:00 2001 From: EvAlex Date: Mon, 22 Jan 2018 17:39:51 +0300 Subject: [PATCH 3/3] generate & refactor code for MPD XSD --- DashTools.CodeGenerator/App.config | 58 + DashTools.CodeGenerator/ClassWalker.cs | 33 + .../DashTools.CodeGenerator.csproj | 243 + .../MoveNamespacesToUsingsRewriter.cs | 137 + DashTools.CodeGenerator/MpdCodeRefactorer.cs | 87 + DashTools.CodeGenerator/Program.cs | 184 + .../Properties/AssemblyInfo.cs | 36 + .../RemoveAttributeSuffixRewriter.cs | 64 + DashTools.CodeGenerator/RenameSymbolInfo.cs | 18 + .../UsingDirectivesFinder.cs | 104 + DashTools.CodeGenerator/packages.config | 62 + DashTools/DashTools.csproj | 4 + DashTools/MpdSchema.cs | 4042 +++++++++++++++++ SharpDashTools.sln | 8 +- 14 files changed, 5079 insertions(+), 1 deletion(-) create mode 100644 DashTools.CodeGenerator/App.config create mode 100644 DashTools.CodeGenerator/ClassWalker.cs create mode 100644 DashTools.CodeGenerator/DashTools.CodeGenerator.csproj create mode 100644 DashTools.CodeGenerator/MoveNamespacesToUsingsRewriter.cs create mode 100644 DashTools.CodeGenerator/MpdCodeRefactorer.cs create mode 100644 DashTools.CodeGenerator/Program.cs create mode 100644 DashTools.CodeGenerator/Properties/AssemblyInfo.cs create mode 100644 DashTools.CodeGenerator/RemoveAttributeSuffixRewriter.cs create mode 100644 DashTools.CodeGenerator/RenameSymbolInfo.cs create mode 100644 DashTools.CodeGenerator/UsingDirectivesFinder.cs create mode 100644 DashTools.CodeGenerator/packages.config create mode 100644 DashTools/MpdSchema.cs diff --git a/DashTools.CodeGenerator/App.config b/DashTools.CodeGenerator/App.config new file mode 100644 index 0000000..44e1a77 --- /dev/null +++ b/DashTools.CodeGenerator/App.config @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DashTools.CodeGenerator/ClassWalker.cs b/DashTools.CodeGenerator/ClassWalker.cs new file mode 100644 index 0000000..3ea4c2f --- /dev/null +++ b/DashTools.CodeGenerator/ClassWalker.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DashTools.CodeGenerator +{ + public class ClassWalker : CSharpSyntaxWalker + { + public ClassWalker(SyntaxWalkerDepth depth = SyntaxWalkerDepth.Node) + : base(depth) + { + } + + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + + { + var token = SyntaxFactory.Identifier( + node.Identifier.LeadingTrivia, + node.Identifier.Kind(), + node.Identifier.ValueText.Replace("type", ""), + node.Identifier.ValueText.Replace("type", ""), + node.Identifier.TrailingTrivia); + var changed = node.WithIdentifier(token); + + base.VisitClassDeclaration(node); + } + } +} diff --git a/DashTools.CodeGenerator/DashTools.CodeGenerator.csproj b/DashTools.CodeGenerator/DashTools.CodeGenerator.csproj new file mode 100644 index 0000000..f6e9f5d --- /dev/null +++ b/DashTools.CodeGenerator/DashTools.CodeGenerator.csproj @@ -0,0 +1,243 @@ + + + + + Debug + AnyCPU + {5B0225A5-45AE-4A44-9BC7-BF60825DFDF4} + Exe + Properties + DashTools.CodeGenerator + DashTools.CodeGenerator + v4.6.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Build.15.5.180\lib\net46\Microsoft.Build.dll + True + + + ..\packages\Microsoft.Build.Framework.15.5.180\lib\net46\Microsoft.Build.Framework.dll + True + + + ..\packages\Microsoft.Build.Tasks.Core.15.5.180\lib\net46\Microsoft.Build.Tasks.Core.dll + True + + + ..\packages\Microsoft.Build.Utilities.Core.15.5.180\lib\net46\Microsoft.Build.Utilities.Core.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Common.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.VisualBasic.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.VisualBasic.dll + True + + + ..\packages\Microsoft.CodeAnalysis.VisualBasic.Workspaces.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.2.6.1\lib\net46\Microsoft.CodeAnalysis.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.2.6.1\lib\net46\Microsoft.CodeAnalysis.Workspaces.Desktop.dll + True + + + + ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll + True + + + ..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + ..\packages\System.Composition.AttributedModel.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll + True + + + ..\packages\System.Composition.Convention.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll + True + + + ..\packages\System.Composition.Hosting.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll + True + + + ..\packages\System.Composition.Runtime.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll + True + + + ..\packages\System.Composition.TypedParts.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll + True + + + ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + True + + + + ..\packages\System.Diagnostics.FileVersionInfo.4.3.0\lib\net46\System.Diagnostics.FileVersionInfo.dll + True + + + ..\packages\System.Diagnostics.StackTrace.4.3.0\lib\net46\System.Diagnostics.StackTrace.dll + True + + + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + True + + + ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + True + + + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + True + + + + ..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll + True + + + ..\packages\System.Reflection.Metadata.1.4.2\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll + True + + + ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll + True + + + ..\packages\System.Runtime.InteropServices.4.3.0\lib\net462\System.Runtime.InteropServices.dll + True + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll + True + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + + + ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + True + + + ..\packages\System.Text.Encoding.CodePages.4.3.0\lib\net46\System.Text.Encoding.CodePages.dll + True + + + ..\packages\System.Threading.Thread.4.3.0\lib\net46\System.Threading.Thread.dll + True + + + ..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll + True + + + + + + + + + ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + True + + + ..\packages\System.Xml.XmlDocument.4.3.0\lib\net46\System.Xml.XmlDocument.dll + True + + + ..\packages\System.Xml.XPath.4.3.0\lib\net46\System.Xml.XPath.dll + True + + + ..\packages\System.Xml.XPath.XDocument.4.3.0\lib\net46\System.Xml.XPath.XDocument.dll + True + + + + + + + + + + + + + + + + + + + + + + + {d517b927-7afc-42ad-a410-27354f3217c2} + DashTools + + + + + \ No newline at end of file diff --git a/DashTools.CodeGenerator/MoveNamespacesToUsingsRewriter.cs b/DashTools.CodeGenerator/MoveNamespacesToUsingsRewriter.cs new file mode 100644 index 0000000..24eddec --- /dev/null +++ b/DashTools.CodeGenerator/MoveNamespacesToUsingsRewriter.cs @@ -0,0 +1,137 @@ +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DashTools.CodeGenerator +{ + public class MoveNamespacesToUsingsRewriter : CSharpSyntaxRewriter + { + private List namespacesToAdd = null; + + public MoveNamespacesToUsingsRewriter(bool visitIntoStructuredTrivia = false) + : base(visitIntoStructuredTrivia) + { + } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (namespacesToAdd == null) + { + var finder = new UsingDirectivesFinder(); + finder.Visit(node); + var existingUsingDirectives = finder.ExistingUsingDirectives.ToList(); + namespacesToAdd = finder + .NamespacesInQualifiedNames + .Where(e => !existingUsingDirectives.Any(ee => ee.ToFullString() == e.ToFullString())) + .ToList(); + } + + node = base.Visit(node); + + return node; + } + + public override SyntaxNode VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) + { + var usingsToAdd = namespacesToAdd + .Select(e => e.WithLeadingTrivia(SyntaxFactory.Whitespace(" "))) + .Select(e => SyntaxFactory.UsingDirective(e).WithLeadingTrivia(SyntaxFactory.Tab)) + .Select(e => e.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)); + var newUsings = node.Usings.AddRange(usingsToAdd); + + return (base.VisitNamespaceDeclaration(node) as NamespaceDeclarationSyntax) + .WithUsings(new SyntaxList(newUsings)); + } + + public override SyntaxNode VisitAttribute(AttributeSyntax node) + { + node = base.VisitAttribute(node) as AttributeSyntax; + + if (node.Name is QualifiedNameSyntax) + { + var qualifiedName = node.Name as QualifiedNameSyntax; + var simpleName = qualifiedName.Right; + node = node.WithName(simpleName); + } + + return node; + } + + public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + node = base.VisitPropertyDeclaration(node) as PropertyDeclarationSyntax; + + var type = GetTypeWithoutNamespaces(node.Type).WithTriviaFrom(node.Type); + node = node + .WithType(type) + .WithTriviaFrom(node); + + return node; + } + + public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node) + { + node = base.VisitFieldDeclaration(node) as FieldDeclarationSyntax; + + var type = GetTypeWithoutNamespaces(node.Declaration.Type) + .WithTriviaFrom(node.Declaration.Type); + var declaration = node.Declaration + .WithType(type) + .WithTriviaFrom(node.Declaration); + node = node.WithDeclaration(declaration); + + return node; + } + + public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) + { + node = base.VisitMethodDeclaration(node) as MethodDeclarationSyntax; + + node = node.WithReturnType(GetTypeWithoutNamespaces(node.ReturnType)) + .WithLeadingTrivia(node.ReturnType.GetLeadingTrivia()) + .WithTrailingTrivia(node.ReturnType.GetTrailingTrivia()); + + + var parameters = node.ParameterList.Parameters; + for (int i = 0; i < parameters.Count; i++) + { + var arg = parameters[i]; + parameters = parameters.Replace( + arg, + arg.WithType(GetTypeWithoutNamespaces(arg.Type)) + .WithLeadingTrivia(arg.Type.GetLeadingTrivia()) + .WithTrailingTrivia(arg.Type.GetTrailingTrivia()) + ); + } + node = node.WithParameterList( + node.ParameterList + .WithParameters(parameters) + .WithLeadingTrivia(node.ParameterList.GetLeadingTrivia()) + .WithTrailingTrivia(node.ParameterList.GetTrailingTrivia()) + ); + + return node; + } + + private TypeSyntax GetTypeWithoutNamespaces(T type) + where T : TypeSyntax + { + QualifiedNameSyntax qualifiedName = null; + if (type is QualifiedNameSyntax) + { + qualifiedName = type as QualifiedNameSyntax; + } + else if (type is ArrayTypeSyntax && (type as ArrayTypeSyntax).ElementType is QualifiedNameSyntax) + { + qualifiedName = (type as ArrayTypeSyntax).ElementType as QualifiedNameSyntax; + } + + return qualifiedName?.Right as TypeSyntax ?? type; + } + } +} diff --git a/DashTools.CodeGenerator/MpdCodeRefactorer.cs b/DashTools.CodeGenerator/MpdCodeRefactorer.cs new file mode 100644 index 0000000..60d742f --- /dev/null +++ b/DashTools.CodeGenerator/MpdCodeRefactorer.cs @@ -0,0 +1,87 @@ +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Rename; + +namespace DashTools.CodeGenerator +{ + public class MpdCodeRefactorer : CSharpSyntaxRewriter + { + private readonly SemanticModel semanticModel; + + public RenameSymbolInfo Refactoring { get; private set; } + + public MpdCodeRefactorer(SemanticModel semanticModel, bool visitIntoStructuredTrivia = false) + : base(visitIntoStructuredTrivia) + { + this.semanticModel = semanticModel; + } + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + string className = node.Identifier.ValueText; + if (className.EndsWith("type")) + { + className = className.Replace("type", ""); + + Refactoring = new RenameSymbolInfo + { + Symbol = semanticModel.GetDeclaredSymbol(node), + NewName = className, + Comment = "Remove \"type\" suffix from class name" + }; + } + + return Refactoring == null + ? base.VisitClassDeclaration(node) + : node; + } + + public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + string name = node.Identifier.ValueText; + if (name.Substring(0, 1) != name.Substring(0, 1).ToUpper()) + { + name = name.Substring(0, 1).ToUpper() + name.Substring(1); + + Refactoring = new RenameSymbolInfo + { + Symbol = semanticModel.GetDeclaredSymbol(node), + NewName = name, + Comment = "Capitalize property name's 1st letter" + }; + } + + return base.VisitPropertyDeclaration(node); + } + + public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node) + { + foreach (var variable in node.Declaration.Variables) + { + string fieldName = variable.Identifier.ValueText; + if (fieldName.EndsWith("Field")) + { + fieldName = fieldName.Substring(0, fieldName.Length - "Field".Length); + + Refactoring = new RenameSymbolInfo + { + Symbol = semanticModel.GetDeclaredSymbol(variable), + NewName = fieldName, + Comment = "Remove \"Field\" suffix from field name" + }; + } + } + + return Refactoring == null + ? base.VisitFieldDeclaration(node) + : node; + } + } +} diff --git a/DashTools.CodeGenerator/Program.cs b/DashTools.CodeGenerator/Program.cs new file mode 100644 index 0000000..e66555a --- /dev/null +++ b/DashTools.CodeGenerator/Program.cs @@ -0,0 +1,184 @@ +using Microsoft.Build.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.Rename; +using Microsoft.CodeAnalysis.Text; +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace DashTools.CodeGenerator +{ + class Program + { + static void Main(string[] args) + { + try + { + MainAsync().Wait(); + } + catch (Exception ex) + { + Console.Write(ex); + } + + Console.WriteLine(); + Console.WriteLine("Press to exit"); + Console.ReadLine(); + } + + private static async Task MainAsync() + { + string filePath = await GenerateCodeForMpdSchema(); + string refactoredFilePath = await RefactorMpdCodeAsync(filePath); + + return; + + + + //var workspace = MSBuildWorkspace.Create(); + //var solution = await workspace.OpenSolutionAsync("../../../SharpDashTools.sln"); + //var project = solution.Projects.Single(p => p.AssemblyName == typeof(Qoollo.MpegDash.MpdDownloader).Assembly.GetName().Name); + //var document = project.Documents.Single(d => d.Name == "MpdSchema.cs"); + //var syntaxTree = await document.GetSyntaxRootAsync(); + + //var walker = new ClassWalker(); + //walker.Visit(syntaxTree); + + //var rewriter = new MpdCodeRefactorer(); + //var changed = rewriter.Visit(syntaxTree); + //File.WriteAllText(document.FilePath + "test.cs", changed.ToFullString()); + + //var generator = SyntaxGenerator.GetGenerator(workspace, LanguageNames.CSharp); + } + + private static async Task GenerateCodeForMpdSchema() + { + var schemaFiles = await DownloadMpdSchemaAsync(); + + string result = GenerateCodeForMpdSchema(schemaFiles); + + foreach (var file in schemaFiles) + File.Delete(file); + + return result; + } + + private static async Task DownloadMpdSchemaAsync() + { + const string xsdUrl = "https://github.com/Dash-Industry-Forum/Conformance-and-reference-source/raw/master/conformance/MPDValidator/schemas/DASH-MPD.xsd"; + const string xsdFile = "DASH-MPD.xsd"; + + const string xlinkUrl = "https://github.com/Dash-Industry-Forum/Conformance-and-reference-source/raw/master/conformance/MPDValidator/schemas/xlink.xsd"; + const string xlinkFile = "xlink.xsd"; + + var webClient = new WebClient(); + await webClient.DownloadFileTaskAsync(xsdUrl, xsdFile); + await webClient.DownloadFileTaskAsync(xlinkUrl, xlinkFile); + + return new[] { xsdFile, xlinkFile }; + } + + private static string GenerateCodeForMpdSchema(IEnumerable xsdFiles) + { + const string xsdExePath = @"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\xsd.exe"; + if (!File.Exists(xsdExePath)) + throw new InvalidOperationException("xsd.exe not found at " + xsdExePath); + + const string newFileName = "MpdSchema.cs"; + if (File.Exists(newFileName)) + File.Delete(newFileName); + + var process = Process.Start(xsdExePath, string.Join(" ", xsdFiles) + " /c /n:Qoollo.MpegDash"); + if (!process.WaitForExit(20000)) + throw new TimeoutException("XSD -> C# code generation takes too long. Something's wrong."); + + string fileName = string.Join("_", xsdFiles.Select(e => Path.GetFileNameWithoutExtension(e))) + ".cs"; + if (!File.Exists(fileName)) + throw new Exception("XSD -> C# code generation succeeded, but file " + fileName + " was not found"); + + File.Move(fileName, newFileName); + + return newFileName; + } + + private static async Task RefactorMpdCodeAsync(string filePath) + { + var workspace = new AdhocWorkspace(); + var projectId = ProjectId.CreateNewId(); + var versionStamp = VersionStamp.Create(); + var projectInfo = ProjectInfo.Create(projectId, versionStamp, "test", "test", LanguageNames.CSharp); + projectInfo = projectInfo.WithMetadataReferences( + new [] + { + typeof(int), + typeof(object), + typeof(GeneratedCodeAttribute), + typeof(XmlTypeAttribute) + } + .Select(e => e.Assembly.Location) + .Select(e => MetadataReference.CreateFromFile(e)) + ); + var project = workspace.AddProject(projectInfo); + + var sourceText = SourceText.From(File.ReadAllText(filePath)); + var document = workspace.AddDocument(project.Id, "NewFile.cs", sourceText); + var syntaxRoot = await document.GetSyntaxRootAsync(); + + + var semanticModel = await document.GetSemanticModelAsync(); + var diagnostics = semanticModel.GetDiagnostics(); + + var rewriters = new CSharpSyntaxRewriter[] + { + new MoveNamespacesToUsingsRewriter(), + new RemoveAttributeSuffixRewriter() + }; + foreach (var rewriter in rewriters) + { + syntaxRoot = rewriter.Visit(syntaxRoot); + + workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentSyntaxRoot(document.Id, syntaxRoot)); + document = workspace.CurrentSolution.GetDocument(document.Id); + syntaxRoot = await document.GetSyntaxRootAsync(); + semanticModel = await document.GetSemanticModelAsync(); + } + + var refactorer = new MpdCodeRefactorer(semanticModel); + syntaxRoot = await document.GetSyntaxRootAsync(); + syntaxRoot = refactorer.Visit(syntaxRoot); + while (refactorer.Refactoring != null) + { + diagnostics = semanticModel.GetDiagnostics(); + + var solution = await Renamer.RenameSymbolAsync(workspace.CurrentSolution, refactorer.Refactoring.Symbol, refactorer.Refactoring.NewName, workspace.Options); + if (!workspace.TryApplyChanges(solution)) + { + throw new InvalidOperationException("Failed to apply changes after rename"); + } + Console.WriteLine("Applied refactoring:\n\t{0}", refactorer.Refactoring); + + document = workspace.CurrentSolution.GetDocument(document.Id); + syntaxRoot = await document.GetSyntaxRootAsync(); + semanticModel = await document.GetSemanticModelAsync(); + refactorer = new MpdCodeRefactorer(semanticModel); + refactorer.Visit(syntaxRoot); + } + + const string newFileName = "NewFileRefactored.cs"; + File.WriteAllText(newFileName, syntaxRoot.ToFullString()); + + return newFileName; + } + + } +} diff --git a/DashTools.CodeGenerator/Properties/AssemblyInfo.cs b/DashTools.CodeGenerator/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4a145b2 --- /dev/null +++ b/DashTools.CodeGenerator/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DashTools.CodeGenerator")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DashTools.CodeGenerator")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5b0225a5-45ae-4a44-9bc7-bf60825dfdf4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DashTools.CodeGenerator/RemoveAttributeSuffixRewriter.cs b/DashTools.CodeGenerator/RemoveAttributeSuffixRewriter.cs new file mode 100644 index 0000000..0fece2c --- /dev/null +++ b/DashTools.CodeGenerator/RemoveAttributeSuffixRewriter.cs @@ -0,0 +1,64 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DashTools.CodeGenerator +{ + public class RemoveAttributeSuffixRewriter : CSharpSyntaxRewriter + { + public RemoveAttributeSuffixRewriter(bool visitIntoStructuredTrivia = false) + : base(visitIntoStructuredTrivia) + { + } + + public override SyntaxNode VisitAttribute(AttributeSyntax node) + { + node = base.VisitAttribute(node) as AttributeSyntax; + + if (node.Name is QualifiedNameSyntax) + { + var qualifiedName = node.Name as QualifiedNameSyntax; + var newRight = RemoveTrailingAttributeFromName(qualifiedName.Right); + var newQualifiedName = qualifiedName.WithRight(newRight); + node = node.WithName(newQualifiedName); + } + else if (node.Name is SimpleNameSyntax) + { + var newName = RemoveTrailingAttributeFromName(node.Name as SimpleNameSyntax); + node = node.WithName(newName); + } + else if (Debugger.IsAttached) + { + Debugger.Break(); + } + + return node; + } + + private SimpleNameSyntax RemoveTrailingAttributeFromName(SimpleNameSyntax simpleName) + { + string attributeName = simpleName.Identifier.ValueText; + if (attributeName.EndsWith("Attribute")) + { + attributeName = attributeName.Substring(0, attributeName.Length - "Attribute".Length); + + var identifier = SyntaxFactory.Identifier( + simpleName.Identifier.LeadingTrivia, + simpleName.Identifier.Kind(), + attributeName, + attributeName, + simpleName.Identifier.TrailingTrivia + ); + simpleName = simpleName.WithIdentifier(identifier); + } + + return simpleName; + } + } +} diff --git a/DashTools.CodeGenerator/RenameSymbolInfo.cs b/DashTools.CodeGenerator/RenameSymbolInfo.cs new file mode 100644 index 0000000..7544f22 --- /dev/null +++ b/DashTools.CodeGenerator/RenameSymbolInfo.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +namespace DashTools.CodeGenerator +{ + public class RenameSymbolInfo + { + public ISymbol Symbol { get; set; } + + public string NewName { get; set; } + + public string Comment { get; set; } + + public override string ToString() + { + return $"{Comment}.\n\tOld name: {Symbol.Name}.\n\tNew name: {NewName}."; + } + } +} \ No newline at end of file diff --git a/DashTools.CodeGenerator/UsingDirectivesFinder.cs b/DashTools.CodeGenerator/UsingDirectivesFinder.cs new file mode 100644 index 0000000..c98fd08 --- /dev/null +++ b/DashTools.CodeGenerator/UsingDirectivesFinder.cs @@ -0,0 +1,104 @@ +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DashTools.CodeGenerator +{ + public class UsingDirectivesFinder : CSharpSyntaxWalker + { + public IEnumerable ExistingUsingDirectives + { + get { return existingUsingDirectives; } + } + private readonly List existingUsingDirectives = new List(); + + public IEnumerable NamespacesInQualifiedNames + { + get { return namespacesInQualifiedNames; } + } + private readonly List namespacesInQualifiedNames = new List(); + + public override void VisitUsingDirective(UsingDirectiveSyntax node) + { + base.VisitUsingDirective(node); + + if (!existingUsingDirectives.Any(e => e.ToFullString() == node.Name.ToFullString())) + existingUsingDirectives.Add(node.Name); + } + + public override void VisitAttribute(AttributeSyntax node) + { + base.VisitAttribute(node); + + if (node.Name is QualifiedNameSyntax) + { + var qualifiedName = node.Name as QualifiedNameSyntax; + if (!namespacesInQualifiedNames.Any(e => e.ToFullString() == qualifiedName.Left.ToFullString())) + { + namespacesInQualifiedNames.Add(qualifiedName.Left); + } + } + } + + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + base.VisitPropertyDeclaration(node); + + ProcessType(node.Type); + } + + public override void VisitFieldDeclaration(FieldDeclarationSyntax node) + { + base.VisitFieldDeclaration(node); + + ProcessType(node.Declaration.Type); + } + + public override void VisitMethodDeclaration(MethodDeclarationSyntax node) + { + base.VisitMethodDeclaration(node); + + ProcessType(node.ReturnType); + + foreach (var arg in node.ParameterList.Parameters) + { + ProcessType(arg.Type); + } + } + + private void ProcessType(TypeSyntax type) + { + var qualifiedNames = new List(); + + if (type is IdentifierNameSyntax) + { + // (type as IdentifierNameSyntax).Identifier + } + else if (type is ArrayTypeSyntax) + { + var elementType = (type as ArrayTypeSyntax).ElementType; + if (elementType is QualifiedNameSyntax) + { + qualifiedNames.Add(elementType as QualifiedNameSyntax); + } + } + else if (type is QualifiedNameSyntax) + { + qualifiedNames.Add(type as QualifiedNameSyntax); + } + + foreach (var qualifiedName in qualifiedNames) + { + if (!namespacesInQualifiedNames.Any(e => e.ToFullString() == qualifiedName.Left.ToFullString())) + { + namespacesInQualifiedNames.Add(qualifiedName.Left); + } + } + } + + } +} diff --git a/DashTools.CodeGenerator/packages.config b/DashTools.CodeGenerator/packages.config new file mode 100644 index 0000000..bb7ca56 --- /dev/null +++ b/DashTools.CodeGenerator/packages.config @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DashTools/DashTools.csproj b/DashTools/DashTools.csproj index 440af9a..77779c6 100644 --- a/DashTools/DashTools.csproj +++ b/DashTools/DashTools.csproj @@ -40,6 +40,7 @@ + @@ -69,6 +70,9 @@ + + +