1 #region MIT license
2 //
3 // MIT license
4 //
5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne
6 //
7 // Permission is hereby granted, free of charge, to any person obtaining a copy
8 // of this software and associated documentation files (the "Software"), to deal
9 // in the Software without restriction, including without limitation the rights
10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 // copies of the Software, and to permit persons to whom the Software is
12 // furnished to do so, subject to the following conditions:
13 //
14 // The above copyright notice and this permission notice shall be included in
15 // all copies or substantial portions of the Software.
16 //
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 // THE SOFTWARE.
24 //
25 #endregion
26 
27 using System;
28 using System.IO;
29 using System.Linq;
30 using System.Reflection;
31 using System.Collections.Generic;
32 using System.Text;
33 using DbLinq.Util;
34 using DbMetal.Utility;
35 
36 namespace DbMetal
37 {
38     /// <summary>
39     /// Parameters base class.
40     /// Allows to specify direct switches or place switches in a file (specified with @fileName).
41     /// If a file specifies several line, the parameters will allow batch processing, one per line.
42     /// Parameters specified before the @ file are inherited by each @ file line
43     /// </summary>
44     public abstract class AbstractParameters
45     {
46         /// <summary>
47         /// Describes a switch (/sprocs)
48         /// </summary>
49         public class OptionAttribute : Attribute
50         {
51             /// <summary>
52             /// Allows to specify a group. All options in the same group are displayed together
53             /// </summary>
54             public int Group { get; set; }
55 
56             /// <summary>
57             /// Description
58             /// </summary>
59             public string Text { get; set; }
60 
61             /// <summary>
62             /// Value name, used for help
63             /// </summary>
64             public string ValueName { get; set; }
65 
OptionAttribute(string text)66             public OptionAttribute(string text)
67             {
68                 Text = text;
69             }
70         }
71 
72         /// <summary>
73         /// Describes an input file
74         /// </summary>
75         public class FileAttribute : Attribute
76         {
77             /// <summary>
78             /// Tells if the file is required
79             /// TODO: add mandatory support in parameters check
80             /// </summary>
81             public bool Mandatory { get; set; }
82             /// <summary>
83             /// The name written in help
84             /// </summary>
85             public string Name { get; set; }
86             /// <summary>
87             /// Descriptions
88             /// </summary>
89             public string Text { get; set; }
90 
FileAttribute(string name, string text)91             public FileAttribute(string name, string text)
92             {
93                 Name = name;
94                 Text = text;
95             }
96         }
97 
98         public class AlternateAttribute : Attribute
99         {
100             public string Name { get; set; }
101 
AlternateAttribute(string name)102             public AlternateAttribute(string name)
103             {
104                 Name = name;
105             }
106         }
107 
108         public readonly IList<string> Extra = new List<string>();
109         private TextWriter log;
110         public TextWriter Log
111         {
112             get { return log ?? Console.Out; }
113             set { log = value; }
114         }
115 
IsParameter(string arg, string switchPrefix, out string parameterName, out string parameterValue)116         private static bool IsParameter(string arg, string switchPrefix, out string parameterName, out string parameterValue)
117         {
118             bool isParameter;
119             if (arg.StartsWith(switchPrefix))
120             {
121                 isParameter = true;
122                 string nameValue = arg.Substring(switchPrefix.Length);
123                 int separator = nameValue.IndexOfAny(new[] { ':', '=' });
124                 if (separator >= 0)
125                 {
126                     parameterName = nameValue.Substring(0, separator);
127                     parameterValue = nameValue.Substring(separator + 1).Trim('\"');
128                 }
129                 else if (nameValue.EndsWith("+"))
130                 {
131                     parameterName = nameValue.Substring(0, nameValue.Length - 1);
132                     parameterValue = "+";
133                 }
134                 else if (nameValue.EndsWith("-"))
135                 {
136                     parameterName = nameValue.Substring(0, nameValue.Length - 1);
137                     parameterValue = "-";
138                 }
139                 else if (nameValue.StartsWith("no-"))
140                 {
141                     parameterName = nameValue.Substring(3);
142                     parameterValue = "-";
143                 }
144                 else
145                 {
146                     parameterName = nameValue;
147                     parameterValue = null;
148                 }
149             }
150             else
151             {
152                 isParameter = false;
153                 parameterName = null;
154                 parameterValue = null;
155             }
156             return isParameter;
157         }
158 
IsParameter(string arg, out string parameterName, out string parameterValue)159         protected static bool IsParameter(string arg, out string parameterName, out string parameterValue)
160         {
161             return IsParameter(arg, "--", out parameterName, out parameterValue)
162                    || IsParameter(arg, "-", out parameterName, out parameterValue)
163                    || IsParameter(arg, "/", out parameterName, out parameterValue);
164         }
165 
GetValue(string value, Type targetType)166         protected static object GetValue(string value, Type targetType)
167         {
168             object typedValue;
169             if (typeof(bool).IsAssignableFrom(targetType))
170             {
171                 if (value == null || value == "+")
172                     typedValue = true;
173                 else if (value == "-")
174                     typedValue = false;
175                 else
176                     typedValue = Convert.ToBoolean(value);
177             }
178             else
179             {
180                 typedValue = Convert.ChangeType(value, targetType);
181             }
182             return typedValue;
183         }
184 
FindParameter(string name, Type type)185         protected virtual MemberInfo FindParameter(string name, Type type)
186         {
187             // the easy way: find propery or field name
188             var flags = BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public;
189             var memberInfos = type.GetMember(name, flags);
190             if (memberInfos.Length > 0)
191                 return memberInfos[0];
192             // the hard way: look for alternate names
193             memberInfos = type.GetMembers();
194             foreach (var memberInfo in memberInfos)
195             {
196                 var alternates = (AlternateAttribute[])memberInfo.GetCustomAttributes(typeof(AlternateAttribute), true);
197                 if (Array.Exists(alternates, a => string.Compare(a.Name, name) == 0))
198                     return memberInfo;
199             }
200             return null;
201         }
202 
FindParameter(string name)203         protected virtual MemberInfo FindParameter(string name)
204         {
205             return FindParameter(name, GetType());
206         }
207 
208         /// <summary>
209         /// Assigns a parameter by reflection
210         /// </summary>
211         /// <param name="name">parameter name (case insensitive)</param>
212         /// <param name="value">parameter value</param>
SetParameter(string name, string value)213         protected void SetParameter(string name, string value)
214         {
215             // cleanup and evaluate
216             name = name.Trim();
217             // evaluate
218             value = value.EvaluateEnvironment();
219 
220             var memberInfo = FindParameter(name);
221             if (memberInfo == null)
222                 throw new ArgumentException(string.Format("Parameter {0} does not exist", name));
223             memberInfo.SetMemberValue(this, GetValue(value, memberInfo.GetMemberType()));
224         }
225 
226         /// <summary>
227         /// Loads arguments from a given list
228         /// </summary>
229         /// <param name="args"></param>
Load(IList<string> args)230         public void Load(IList<string> args)
231         {
232             foreach (string arg in args)
233             {
234                 string key, value;
235                 if (IsParameter(arg, out key, out value))
236                     SetParameter(key, value);
237                 else
238                     Extra.Add(arg);
239             }
240         }
241 
AbstractParameters()242         protected AbstractParameters()
243         {
244         }
245 
AbstractParameters(IList<string> args)246         protected AbstractParameters(IList<string> args)
247         {
248             Load(args);
249         }
250 
251         /// <summary>
252         /// Internal method allowing to extract arguments and specify quotes characters
253         /// </summary>
254         /// <param name="commandLine"></param>
255         /// <param name="quotes"></param>
256         /// <returns></returns>
ExtractArguments(string commandLine, char[] quotes)257         public IList<string> ExtractArguments(string commandLine, char[] quotes)
258         {
259             var arg = new StringBuilder();
260             var args = new List<string>();
261             const char zero = '\0';
262             char quote = zero;
263             foreach (char c in commandLine)
264             {
265                 if (quote == zero)
266                 {
267                     if (quotes.Contains(c))
268                         quote = c;
269                     else if (char.IsSeparator(c) && quote == zero)
270                     {
271                         if (arg.Length > 0)
272                         {
273                             args.Add(arg.ToString());
274                             arg = new StringBuilder();
275                         }
276                     }
277                     else
278                         arg.Append(c);
279                 }
280                 else
281                 {
282                     if (c == quote)
283                         quote = zero;
284                     else
285                         arg.Append(c);
286                 }
287             }
288             if (arg.Length > 0)
289                 args.Add(arg.ToString());
290             return args;
291         }
292 
293         private static readonly char[] Quotes = new[] { '\'', '\"' };
294         /// <summary>
295         /// Extracts arguments from a full line, in a .NET compatible way
296         /// (includes strange quotes trimming)
297         /// </summary>
298         /// <param name="commandLine">The command line</param>
299         /// <returns>Arguments list</returns>
ExtractArguments(string commandLine)300         public IList<string> ExtractArguments(string commandLine)
301         {
302             return ExtractArguments(commandLine, Quotes);
303         }
304 
305         /// <summary>
306         /// Converts a list separated by a comma to a string array
307         /// </summary>
308         /// <param name="list"></param>
309         /// <returns></returns>
GetArray(string list)310         public string[] GetArray(string list)
311         {
312             if (string.IsNullOrEmpty(list))
313                 return new string[0];
314             return (from entityInterface in list.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
315                     select entityInterface.Trim()).ToArray();
316         }
317 
318         /// <summary>
319         /// Processes different "lines" of parameters:
320         /// 1. the original input parameter must be starting with @
321         /// 2. all other parameters are kept as a common part
322         /// </summary>
323         /// <typeparam name="P"></typeparam>
324         /// <param name="args"></param>
325         /// <returns></returns>
326         protected IList<P> GetParameterBatch<P>(IList<string> args)
327             where P : AbstractParameters, new()
328         {
329             return GetParameterBatch<P>(args, ".");
330         }
331 
332         public IList<P> GetParameterBatch<P>(IList<string> args, string argsFileDirectory)
333             where P : AbstractParameters, new()
334         {
335             var parameters = new List<P>();
336             var commonArgs = new List<string>();
337             var argsFiles = new List<string>();
338             foreach (var arg in args)
339             {
340                 if (arg.StartsWith("@"))
341                     argsFiles.Add(arg.Substring(1));
342                 else
343                     commonArgs.Add(arg);
344             }
345             // if we specify files, we must recurse
346             if (argsFiles.Count > 0)
347             {
348                 foreach (var argsFile in argsFiles)
349                 {
350                     parameters.AddRange(GetParameterBatchFile<P>(commonArgs, Path.Combine(argsFileDirectory, argsFile)));
351                 }
352             }
353             // if we don't, just use the args
354             else if (commonArgs.Count > 0)
355             {
356                 var p = new P { Log = Log };
357                 p.Load(commonArgs);
358                 parameters.Add(p);
359             }
360             return parameters;
361         }
362 
363         private IList<P> GetParameterBatchFile<P>(IEnumerable<string> baseArgs, string argsList)
364             where P : AbstractParameters, new()
365         {
366             var parameters = new List<P>();
367             string argsFileDirectory = Path.GetDirectoryName(argsList);
368             using (var textReader = File.OpenText(argsList))
369             {
370                 while (!textReader.EndOfStream)
371                 {
372                     string line = textReader.ReadLine();
373                     if (line.StartsWith("#"))
374                         continue;
375                     var args = ExtractArguments(line);
376                     var allArgs = new List<string>(baseArgs);
377                     allArgs.AddRange(args);
378                     parameters.AddRange(GetParameterBatch<P>(allArgs, argsFileDirectory));
379                 }
380             }
381             return parameters;
382         }
383 
384         /// <summary>
385         /// Outputs a formatted string to the console.
386         /// We're not using the ILogger here, since we want console output.
387         /// </summary>
388         /// <param name="format"></param>
389         /// <param name="args"></param>
Write(string format, params object[] args)390         public void Write(string format, params object[] args)
391         {
392             Output.WriteLine(Log, OutputLevel.Information, format, args);
393         }
394 
395         /// <summary>
396         /// Outputs an empty line
397         /// </summary>
WriteLine()398         public void WriteLine()
399         {
400             Output.WriteLine(Log, OutputLevel.Information, string.Empty);
401         }
402 
403         // TODO: remove this
404         protected static int TextWidth
405         {
406             get { return Console.BufferWidth; }
407         }
408 
409         /// <summary>
410         /// Returns the application (assembly) name (without extension)
411         /// </summary>
412         protected static string ApplicationName
413         {
414             get
415             {
416                 return Assembly.GetEntryAssembly().GetName().Name;
417             }
418         }
419 
420         /// <summary>
421         /// Returns the application (assembly) version
422         /// </summary>
423         protected static Version ApplicationVersion
424         {
425             get
426             {
427                 // Assembly.GetEntryAssembly() is null when loading from the
428                 // non-default AppDomain.
429                 var a = Assembly.GetEntryAssembly();
430                 return a != null ? a.GetName().Version : new Version();
431             }
432         }
433 
434         private bool headerWritten;
435         /// <summary>
436         /// Writes the application header
437         /// </summary>
WriteHeader()438         public void WriteHeader()
439         {
440             if (!headerWritten)
441             {
442                 WriteHeaderContents();
443                 WriteLine();
444                 headerWritten = true;
445             }
446         }
447 
WriteHeaderContents()448         protected abstract void WriteHeaderContents();
449 
450         /// <summary>
451         /// Writes a small summary
452         /// </summary>
WriteSummary()453         public abstract void WriteSummary();
454 
455         /// <summary>
456         /// Writes examples
457         /// </summary>
WriteExamples()458         public virtual void WriteExamples()
459         {
460         }
461 
462         /// <summary>
463         /// The "syntax" is a bried containing the application name, "[options]" and eventually files.
464         /// For example: "DbMetal [options] [&lt;input file>]
465         /// </summary>
WriteSyntax()466         public virtual void WriteSyntax()
467         {
468             var syntax = new StringBuilder();
469             syntax.AppendFormat("{0} [options]", ApplicationName);
470             foreach (var file in GetFiles())
471             {
472                 if (file.Description.Mandatory)
473                     syntax.AppendFormat(" {0}", GetFileText(file));
474                 else
475                     syntax.AppendFormat(" [{0}]", GetFileText(file));
476             }
477             Write(syntax.ToString());
478         }
479 
480         /// <summary>
481         /// Describes an option
482         /// </summary>
483         protected class Option
484         {
485             /// <summary>
486             /// The member name (property or field)
487             /// </summary>
488             public string Name { get; set; }
489             /// <summary>
490             /// The attribute used to define the member as an option
491             /// </summary>
492             public OptionAttribute Description { get; set; }
493         }
494 
495         /// <summary>
496         /// Describes an input file
497         /// </summary>
498         protected class FileName
499         {
500             /// <summary>
501             /// The member name (property or field)
502             /// </summary>
503             public string Name { get; set; }
504             /// <summary>
505             /// The attribute used to define the member as an input file
506             /// </summary>
507             public FileAttribute Description { get; set; }
508         }
509 
510         /// <summary>
511         /// Internal class. I wrote it because I was thinking that the .NET framework already had such a class.
512         /// At second thought, I may have made a confusion with STL
513         /// (interesting, isn't it?)
514         /// </summary>
515         /// <typeparam name="A"></typeparam>
516         /// <typeparam name="B"></typeparam>
517         protected class Pair<A, B>
518         {
519             public A First { get; set; }
520             public B Second { get; set; }
521         }
522 
523         /// <summary>
524         /// Enumerates all members (fields or properties) that have been marked with the specified attribute
525         /// </summary>
526         /// <typeparam name="T">The attribute type to search for</typeparam>
527         /// <returns>A list of pairs with name and attribute</returns>
528         protected IEnumerable<Pair<string, T>> EnumerateOptions<T>()
529             where T : Attribute
530         {
531             Type t = GetType();
532             foreach (var propertyInfo in t.GetProperties())
533             {
534                 var descriptions = (T[])propertyInfo.GetCustomAttributes(typeof(T), true);
535                 if (descriptions.Length == 1)
536                     yield return new Pair<string, T> { First = propertyInfo.Name, Second = descriptions[0] };
537             }
538             foreach (var fieldInfo in t.GetFields())
539             {
540                 var descriptions = (T[])fieldInfo.GetCustomAttributes(typeof(T), true);
541                 if (descriptions.Length == 1)
542                     yield return new Pair<string, T> { First = fieldInfo.Name, Second = descriptions[0] };
543             }
544         }
545 
EnumerateOptions()546         protected IEnumerable<Option> EnumerateOptions()
547         {
548             foreach (var pair in EnumerateOptions<OptionAttribute>())
549                 yield return new Option { Name = pair.First, Description = pair.Second };
550         }
551 
GetFiles()552         protected IEnumerable<FileName> GetFiles()
553         {
554             foreach (var pair in from p in EnumerateOptions<FileAttribute>() orderby p.Second.Mandatory select p)
555                 yield return new FileName { Name = pair.First, Description = pair.Second };
556         }
557 
558         /// <summary>
559         /// Returns options, grouped by group (the group number is the dictionary key)
560         /// </summary>
561         /// <returns></returns>
GetOptions()562         protected IDictionary<int, IList<Option>> GetOptions()
563         {
564             var options = new Dictionary<int, IList<Option>>();
565             foreach (var option in EnumerateOptions())
566             {
567                 if (!options.ContainsKey(option.Description.Group))
568                     options[option.Description.Group] = new List<Option>();
569                 options[option.Description.Group].Add(option);
570             }
571             return options;
572         }
573 
574         /// <summary>
575         /// Return a literal value based on an option
576         /// </summary>
577         /// <param name="option"></param>
578         /// <returns></returns>
GetOptionText(Option option)579         protected virtual string GetOptionText(Option option)
580         {
581             var optionName = option.Name[0].ToString().ToLower() + option.Name.Substring(1);
582             if (string.IsNullOrEmpty(option.Description.ValueName))
583                 return optionName;
584             return string.Format("{0}:<{1}>",
585                 optionName,
586                 option.Description.ValueName);
587         }
588 
589         /// <summary>
590         /// Returns a literal value base on an input file
591         /// </summary>
592         /// <param name="fileName"></param>
593         /// <returns></returns>
GetFileText(FileName fileName)594         protected virtual string GetFileText(FileName fileName)
595         {
596             return string.Format("<{0}>", fileName.Description.Name);
597         }
598 
599         /// <summary>
600         /// Computes the maximum options and files length, to align all descriptions
601         /// </summary>
602         /// <param name="options"></param>
603         /// <param name="files"></param>
604         /// <returns></returns>
GetMaximumLength(IDictionary<int, IList<Option>> options, IEnumerable<FileName> files)605         private int GetMaximumLength(IDictionary<int, IList<Option>> options, IEnumerable<FileName> files)
606         {
607             int maxLength = 0;
608             foreach (var optionsList in options.Values)
609             {
610                 foreach (var option in optionsList)
611                 {
612                     var optionName = GetOptionText(option);
613                     int length = optionName.Length;
614                     if (length > maxLength)
615                         maxLength = length;
616                 }
617             }
618             foreach (var file in files)
619             {
620                 var fileName = GetFileText(file);
621                 int length = fileName.Length;
622                 if (length > maxLength)
623                     maxLength = length;
624             }
625             return maxLength;
626         }
627 
SplitText(string text, int width)628         protected static string[] SplitText(string text, int width)
629         {
630             var lines = new List<string>(new[] { "" });
631             var words = text.Split(' ');
632             foreach (var word in words)
633             {
634                 var line = lines.Last();
635                 if (line.Length == 0)
636                     lines[lines.Count - 1] = word;
637                 else if (line.Length + word.Length + 1 < width)
638                     lines[lines.Count - 1] = line + " " + word;
639                 else
640                     lines.Add(word);
641             }
642             return lines.ToArray();
643         }
644 
WriteOption(string firstLine, string text)645         protected void WriteOption(string firstLine, string text)
646         {
647             int width = TextWidth - firstLine.Length - 2;
648             var lines = SplitText(text, width);
649             var padding = string.Empty.PadRight(firstLine.Length);
650             for (int i = 0; i < lines.Length; i++)
651             {
652                 Write("{0} {1}", i == 0 ? firstLine : padding, lines[i]);
653             }
654         }
655 
656         /// <summary>
657         /// Displays all available options and files
658         /// </summary>
WriteOptions()659         protected void WriteOptions()
660         {
661             var options = GetOptions();
662             var files = GetFiles();
663             int maxLength = GetMaximumLength(options, files);
664             Write("Options:");
665             foreach (var group in from k in options.Keys orderby k select k)
666             {
667                 var optionsList = options[group];
668                 foreach (var option in from o in optionsList orderby o.Name select o)
669                 {
670                     WriteOption(string.Format("  /{0}", GetOptionText(option).PadRight(maxLength)), option.Description.Text);
671                 }
672                 WriteLine();
673             }
674             foreach (var file in files)
675             {
676                 WriteOption(string.Format("  {0}", GetFileText(file).PadRight(maxLength + 1)), file.Description.Text);
677             }
678         }
679 
680         /// <summary>
681         /// Displays application help
682         /// </summary>
WriteHelp()683         public void WriteHelp()
684         {
685             WriteHeader(); // includes a WriteLine()
686             WriteSyntax();
687             WriteLine();
688             WriteSummary();
689             WriteLine();
690             WriteOptions();
691             WriteLine();
692             WriteExamples();
693         }
694     }
695 }
696