using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Xsl;
using google.protobuf;
using System.Xml;
using System.Text;
using System.Xml.Serialization;
using System.Runtime.CompilerServices;
using System.ComponentModel;
using System.Diagnostics;
namespace ProtoBuf.CodeGenerator
{
public sealed class CommandLineOptions
{
private TextWriter errorWriter = Console.Error;
private string workingDirectory = Environment.CurrentDirectory;
///
/// Root directory for the session
///
public string WorkingDirectory
{
get { return workingDirectory; }
set { workingDirectory = value; }
}
///
/// Nominates a writer for error messages (else stderr is used)
///
public TextWriter ErrorWriter
{
get { return errorWriter; }
set { errorWriter = value; }
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Main(params string[] args)
{
CommandLineOptions opt = null;
try
{
opt = Parse(Console.Out, args);
opt.Execute();
return opt.ShowHelp ? 1 : 0; // count help as a non-success (we didn't generate code)
}
catch (Exception ex)
{
Console.Error.Write(ex.Message);
return 1;
}
}
private string template = TemplateCSharp, outPath = "", defaultNamespace;
private bool showLogo = true, showHelp, writeErrorsToFile;
private readonly List inPaths = new List();
private readonly List args = new List();
private int messageCount;
public int MessageCount { get { return messageCount; } }
public bool WriteErrorsToFile { get { return writeErrorsToFile; } set { writeErrorsToFile = value; } }
public string Template { get { return template; } set { template = value; } }
public string DefaultNamespace { get { return defaultNamespace; } set { defaultNamespace = value; } }
public bool ShowLogo { get { return showLogo; } set { showLogo = value; } }
public string OutPath { get { return outPath; } set { outPath = value; } }
public bool ShowHelp { get { return showHelp; } set { showHelp = value; } }
private readonly XsltArgumentList xsltOptions = new XsltArgumentList();
public XsltArgumentList XsltOptions { get { return xsltOptions; } }
public List InPaths { get { return inPaths; } }
public List Arguments { get { return args; } }
private readonly TextWriter messageOutput;
public static CommandLineOptions Parse(TextWriter messageOutput, params string[] args)
{
CommandLineOptions options = new CommandLineOptions(messageOutput);
string key, value;
for (int i = 0; i < args.Length; i++)
{
string arg = args[i].Trim();
if (arg.StartsWith("-o:"))
{
if (!string.IsNullOrEmpty(options.OutPath)) options.ShowHelp = true;
options.OutPath = arg.Substring(3).Trim();
}
else if (arg.StartsWith("-p:"))
{
Split(arg.Substring(3), out key, out value);
options.XsltOptions.AddParam(key, "", value ?? "true");
}
else if (arg.StartsWith("-t:"))
{
options.Template = arg.Substring(3).Trim();
}
else if (arg.StartsWith("-ns:"))
{
options.DefaultNamespace = arg.Substring(4).Trim();
}
else if (arg == "/?" || arg == "-h")
{
options.ShowHelp = true;
}
else if (arg == "-q") // quiet
{
options.ShowLogo = false;
}
else if (arg == "-d")
{
options.Arguments.Add("--include_imports");
}
else if (arg.StartsWith("-i:"))
{
options.InPaths.Add(arg.Substring(3).Trim());
}
else if (arg == "-writeErrors")
{
options.WriteErrorsToFile = true;
}
else if (arg.StartsWith("-w:"))
{
options.WorkingDirectory = arg.Substring(3).Trim();
}
else
{
options.ShowHelp = true;
}
}
if (options.InPaths.Count == 0)
{
options.ShowHelp = (string)options.XsltOptions.GetParam("help", "") != "true";
}
return options;
}
static readonly char[] SplitTokens = { '=' };
private static void Split(string arg, out string key, out string value)
{
string[] parts = arg.Trim().Split(SplitTokens, 2);
key = parts[0].Trim();
value = parts.Length > 1 ? parts[1].Trim() : null;
}
public CommandLineOptions(TextWriter messageOutput)
{
if (messageOutput == null) throw new ArgumentNullException("messageOutput");
this.messageOutput = messageOutput;
// handling this (even trivially) suppresses the default write;
// we'll also use it to track any messages that are generated
XsltOptions.XsltMessageEncountered += delegate { messageCount++; };
}
public const string TemplateCSharp = "csharp";
private string code;
public string Code { get { return code; } private set { code = value; } }
public void Execute()
{
StringBuilder errors = new StringBuilder();
string oldDir = Environment.CurrentDirectory;
Environment.CurrentDirectory = WorkingDirectory;
try
{
if (string.IsNullOrEmpty(OutPath))
{
WriteErrorsToFile = false; // can't be
}
else if (WriteErrorsToFile)
{
ErrorWriter = new StringWriter(errors);
}
try
{
if (ShowLogo)
{
StringBuilder sb = new StringBuilder();
for(int i = 0; i < InPaths.Count; i++)
{
sb.Append(InPaths[i]);
if (i < InPaths.Count - 1)
{
sb.Append(";");
}
}
messageOutput.WriteLine(Properties.Resources.LogoText + " : " + sb);
}
if (ShowHelp)
{
messageOutput.WriteLine(Properties.Resources.Usage);
return;
}
string xml = LoadFilesAsXml(this);
Code = ApplyTransform(this, xml);
if (this.OutPath == "-") { }
else if (!string.IsNullOrEmpty(this.OutPath))
{
File.WriteAllText(this.OutPath, Code);
}
else if (string.IsNullOrEmpty(this.OutPath))
{
messageOutput.Write(Code);
}
}
catch (Exception ex)
{
if (WriteErrorsToFile)
{
// if we had a parse fail and were able to capture something
// sensible, then just write that; otherwise use the exception
// as well
string body = (ex is ProtoParseException && errors.Length > 0) ?
errors.ToString() : (ex.Message + Environment.NewLine + errors);
File.WriteAllText(this.OutPath, body);
}
throw;
}
}
finally
{
try { Environment.CurrentDirectory = oldDir; }
catch (Exception ex) { Trace.WriteLine(ex); }
}
}
private static string LoadFilesAsXml(CommandLineOptions options)
{
FileDescriptorSet set = new FileDescriptorSet();
foreach (string inPath in options.InPaths)
{
InputFileLoader.Merge(set, inPath, options.ErrorWriter, options.Arguments.ToArray());
}
set = ApplyComment(set);
XmlSerializer xser = new XmlSerializer(typeof(FileDescriptorSet));
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = " ";
settings.NewLineHandling = NewLineHandling.Entitize;
StringBuilder sb = new StringBuilder();
using (XmlWriter writer = XmlWriter.Create(sb, settings))
{
xser.Serialize(writer, set);
}
return sb.ToString();
}
private static string ApplyTransform(CommandLineOptions options, string xml)
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.ConformanceLevel = ConformanceLevel.Auto;
settings.CheckCharacters = false;
StringBuilder sb = new StringBuilder();
using (XmlReader reader = XmlReader.Create(new StringReader(xml)))
using (TextWriter writer = new StringWriter(sb))
{
XslCompiledTransform xslt = new XslCompiledTransform();
string xsltTemplate = Path.ChangeExtension(options.Template, "xslt");
if (!File.Exists(xsltTemplate))
{
string localXslt = InputFileLoader.CombinePathFromAppRoot(xsltTemplate);
if (File.Exists(localXslt))
xsltTemplate = localXslt;
}
try
{
xslt.Load(xsltTemplate);
}
catch (Exception ex)
{
throw new InvalidOperationException("Unable to load tranform: " + options.Template, ex);
}
options.XsltOptions.RemoveParam("defaultNamespace", "");
if (options.DefaultNamespace != null)
{
options.XsltOptions.AddParam("defaultNamespace", "", options.DefaultNamespace);
}
xslt.Transform(reader, options.XsltOptions, writer);
}
return sb.ToString();
}
private static void ApplyComment(FileDescriptorProto file, SourceCodeInfo.Location location)
{
if (!string.IsNullOrEmpty(location.leading_comments) ||
!string.IsNullOrEmpty(location.trailing_comments))
{
switch (location.path.Count)
{
case 1:
if (location.path[0] == 2)
{
file.comments = GenComments(location);
}
break;
case 2:
if (location.path[0] == 4)
{
// message define
file.message_type[location.path[1]].comments = GenComments(location);
}
else if (location.path[0] == 5)
{
// enum define
file.enum_type[location.path[1]].comments = GenComments(location);
}
else if (location.path[0] == 6)
{
// service define
file.service[location.path[1]].comments = GenComments(location);
}
break;
case 4:
if (location.path[0] == 4 && location.path[2] == 2)
{
// message fields
file.message_type[location.path[1]].field[location.path[3]].comments = GenComments(location);
}
else if (location.path[0] == 4 && location.path[2] == 4)
{
// message enums
file.message_type[location.path[1]].enum_type[location.path[3]].comments = GenComments(location);
}
else if (location.path[0] == 5 && location.path[2] == 2)
{
// enum values
file.enum_type[location.path[1]].value[location.path[3]].comments = GenComments(location);
}
else if (location.path[0] == 6 && location.path[2] == 2)
{
// service methods
file.service[location.path[1]].method[location.path[3]].comments = GenComments(location);
}
break;
case 6:
if (location.path[0] == 4 && location.path[2] == 4 && location.path[4] == 2)
{
// message inner enums values
file.message_type[location.path[1]].enum_type[location.path[3]].value[location.path[5]].comments = GenComments(location);
}
break;
}
}
}
private static List GenComments(SourceCodeInfo.Location location)
{
var ret = new List();
if (location.path.Count > 1)
{
if (!string.IsNullOrEmpty(location.leading_comments))
{
ret.Add(GenCommentLine(location.leading_comments));
}
if (!string.IsNullOrEmpty(location.trailing_comments))
{
ret.Add(GenCommentLine(location.trailing_comments));
}
}
else
{
StringBuilder sb = new StringBuilder();
if (!string.IsNullOrEmpty(location.leading_comments))
{
sb.Append(location.leading_comments);
}
if (!string.IsNullOrEmpty(location.trailing_comments))
{
sb.AppendLine();
sb.Append(location.trailing_comments);
}
var lines = sb.ToString().Split('\n');
ret.AddRange(lines);
}
return ret;
}
public static string GenCommentLine(string comment)
{
var lines = comment.Trim().Split('\n');
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lines.Length; i++)
{
sb.Append(lines[i]);
if (i < lines.Length - 1)
{
sb.Append(CommentSplitChar);
}
}
return sb.ToString();
}
public static string GenIndentPrefix(int indent)
{
if (indent == 0) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < indent; i++)
{
sb.Append(CommentIndentChar);
}
return sb.ToString().Trim();
}
private static FileDescriptorSet ApplyComment(FileDescriptorSet set)
{
try
{
foreach (var file in set.file)
{
foreach (var location in file.source_code_info.location)
{
if (location.path != null)
{
ApplyComment(file, location);
}
}
}
}
catch (Exception err)
{
Trace.WriteLine(err);
}
return set;
}
public static string CommentIndentChar = " ";
public static string CommentSplitChar = " ";
}
}