// NAnt - A .NET build tool // Copyright (C) 2001-2003 Scott Hernandez // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Scott Hernandez (ScottHernandez@hotmail.com) // Jaroslaw Kowalski (jkowalski@users.sourceforge.net) using System; using System.Collections; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Security.Permissions; using System.Xml; using System.Xml.Schema; using NAnt.Core.Attributes; using NAnt.Core.Util; namespace NAnt.Core.Tasks { /// /// Creates an XSD File for all available tasks. /// /// /// /// This can be used in conjuntion with the command-line option to do XSD /// Schema validation on the build file. /// /// /// /// Creates a NAnt.xsd file in the current project directory. /// /// /// ]]> /// /// [TaskName("nantschema")] public class NAntSchemaTask : Task { #region Private Instance Fields private FileInfo _outputFile; private string _forType = null; private string _targetNamespace = "http://tempuri.org/nant-donotuse.xsd"; #endregion Private Instance Fields #region Private Static Fields private static readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); #endregion Private Static Fields #region Public Instance Properties /// /// The name of the output file to which the XSD should be written. /// [TaskAttribute("output", Required=true)] public virtual FileInfo OutputFile { get { return _outputFile; } set { _outputFile = value; } } /// /// The target namespace for the output. Defaults to "http://tempuri.org/nant-donotuse.xsd" /// [TaskAttribute("target-ns", Required=false)] public virtual string TargetNamespace { get { return _targetNamespace; } set { _targetNamespace = StringUtils.ConvertEmptyToNull(value); } } /// /// The for which an XSD should be created. If not /// specified, an XSD will be created for all available tasks. /// [TaskAttribute("class", Required=false)] public virtual string ForType { get { return _forType; } set { _forType = StringUtils.ConvertEmptyToNull(value); } } #endregion Public Instance Properties #region Override implementation of Task [ReflectionPermission(SecurityAction.Demand, Flags=ReflectionPermissionFlag.NoFlags)] protected override void ExecuteTask() { ArrayList taskTypes; ArrayList dataTypes; if (ForType == null) { taskTypes = new ArrayList(TypeFactory.TaskBuilders.Count); dataTypes = new ArrayList(TypeFactory.DataTypeBuilders.Count); foreach (TaskBuilder tb in TypeFactory.TaskBuilders) { taskTypes.Add(tb.Assembly.GetType(tb.ClassName, true, true)); } foreach (DataTypeBaseBuilder db in TypeFactory.DataTypeBuilders) { dataTypes.Add(db.Assembly.GetType(db.ClassName, true, true)); } } else { taskTypes = new ArrayList(1); taskTypes.Add(Type.GetType(ForType, true, true)); dataTypes = new ArrayList(); } FileIOPermission FilePermission = new FileIOPermission(FileIOPermissionAccess.AllAccess, OutputFile.FullName); FilePermission.Assert(); using (FileStream file = File.Open(OutputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.Read)) { WriteSchema(file, (Type[]) taskTypes.ToArray(typeof(Type)), (Type[]) dataTypes.ToArray(typeof(Type)), TargetNamespace); file.Flush(); file.Close(); } Log(Level.Info, "Wrote schema to '{0}'.", OutputFile.FullName); } #endregion Override implementation of Task #region Public Static Methods /// /// Creates a NAnt Schema for given types /// /// The output stream to save the schema to. If , writing is ignored, no exception generated. /// The list of tasks to generate XML Schema for. /// The list of datatypes to generate XML Schema for. /// The target namespace to output. /// The new NAnt Schema. public static XmlSchema WriteSchema(System.IO.Stream stream, Type[] tasks, Type[] dataTypes, string targetNS) { NAntSchemaGenerator gen = new NAntSchemaGenerator(tasks, dataTypes, targetNS); if (!gen.Schema.IsCompiled) { gen.Compile(); } if (stream != null) { gen.Schema.Write(stream); } return gen.Schema; } #endregion Public Static Methods #region Protected Static Methods protected static string GenerateIDFromType(Type type) { return type.ToString().Replace("+", "-").Replace("[", "_").Replace("]", "_"); } /// /// Creates a new instance. /// /// The name of the attribute. /// Value indicating whether the attribute should be required. /// The new instance. protected static XmlSchemaAttribute CreateXsdAttribute(string name, bool required) { XmlSchemaAttribute newAttr = new XmlSchemaAttribute(); newAttr.Name= name; if (required) { newAttr.Use = XmlSchemaUse.Required; } else { newAttr.Use = XmlSchemaUse.Optional; } return newAttr; } /// /// Creates a new instance. /// /// The minimum value to allow for this choice /// The maximum value to allow, Decimal.MaxValue sets it to 'unbound' /// The new instance. protected static XmlSchemaSequence CreateXsdSequence(Decimal min, Decimal max) { XmlSchemaSequence newSeq = new XmlSchemaSequence(); newSeq.MinOccurs = min; if (max != Decimal.MaxValue) { newSeq.MaxOccurs = max; } else { newSeq.MaxOccursString = "unbounded"; } return newSeq; } protected static XmlNode[] TextToNodeArray(string text) { XmlDocument doc = new XmlDocument(); return new XmlNode[1] {doc.CreateTextNode(text)}; } #endregion Protected Static Methods private class NAntSchemaGenerator { #region Private Instance Fields private IDictionary _nantComplexTypes; private XmlSchemaComplexType _targetCT; private XmlSchema _nantSchema = new XmlSchema(); #endregion Private Instance Fields #region Public Instance Constructors /// /// Creates a new instance of the /// class. /// /// Tasks for which a schema should be generated. /// Data Types for which a schema should be generated. /// The namespace to use. /// http://tempuri.org/nant.xsd /// public NAntSchemaGenerator(Type[] tasks, Type[] dataTypes, string targetNS) { //setup namespace stuff if (targetNS != null) { _nantSchema.TargetNamespace = targetNS; _nantSchema.Namespaces.Add("nant", _nantSchema.TargetNamespace); } // add XSD namespace so that all xsd elements are prefix'd _nantSchema.Namespaces.Add("xs", XmlSchema.Namespace); _nantSchema.ElementFormDefault = XmlSchemaForm.Qualified; // initialize stuff _nantComplexTypes = new HybridDictionary(tasks.Length + dataTypes.Length); XmlSchemaAnnotation schemaAnnotation = new XmlSchemaAnnotation(); XmlSchemaDocumentation schemaDocumentation = new XmlSchemaDocumentation(); string doc = String.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("String_SchemaGenerated"), DateTime.Now); schemaDocumentation.Markup = TextToNodeArray(doc); schemaAnnotation.Items.Add(schemaDocumentation); _nantSchema.Items.Add(schemaAnnotation); // create temp list of taskcontainer Complex Types ArrayList taskContainerComplexTypes = new ArrayList(4); XmlSchemaComplexType containerCT = FindOrCreateComplexType(typeof(TaskContainer)); if (containerCT.Particle == null) { // just create empty sequence to which elements will // be added later containerCT.Particle = CreateXsdSequence(0, Decimal.MaxValue); } taskContainerComplexTypes.Add(containerCT); // create temp list of task Complex Types ArrayList dataTypeComplexTypes = new ArrayList(dataTypes.Length); foreach (Type t in dataTypes) { dataTypeComplexTypes.Add(FindOrCreateComplexType(t)); } foreach (Type t in tasks) { XmlSchemaComplexType taskCT = FindOrCreateComplexType(t); // allow any tasks... if (t.IsSubclassOf(typeof(TaskContainer))) { taskContainerComplexTypes.Add(taskCT); } } Compile(); // update the taskcontainerCTs to allow any other task and the // list of tasks generated foreach(XmlSchemaComplexType ct in taskContainerComplexTypes) { XmlSchemaSequence seq = ct.Particle as XmlSchemaSequence; if (seq != null) { seq.Items.Add(CreateTaskListComplexType(tasks).Particle); } else { logger.Error("Unable to fixup complextype with children. Particle is not XmlSchemaSequence"); } } Compile(); // create target ComplexType _targetCT = CreateTaskListComplexType(tasks, dataTypes, false); _targetCT.Name = "Target"; // name attribute _targetCT.Attributes.Add(CreateXsdAttribute("name", true)); // depends attribute _targetCT.Attributes.Add(CreateXsdAttribute("depends", false)); // description attribute _targetCT.Attributes.Add(CreateXsdAttribute("description", false)); // if attribute _targetCT.Attributes.Add(CreateXsdAttribute("if", false)); // unless attribute _targetCT.Attributes.Add(CreateXsdAttribute("unless", false)); _nantSchema.Items.Add(_targetCT); Compile(); // Generate project Element and ComplexType XmlSchemaElement projectElement = new XmlSchemaElement(); projectElement.Name = "project"; XmlSchemaComplexType projectCT = CreateTaskListComplexType(tasks, dataTypes, true); projectElement.SchemaType = projectCT; //name attribute projectCT.Attributes.Add(CreateXsdAttribute("name", true)); //default attribute projectCT.Attributes.Add(CreateXsdAttribute("default", false)); //basedir attribute projectCT.Attributes.Add(CreateXsdAttribute("basedir", false)); _nantSchema.Items.Add(projectElement); Compile(); } #endregion Public Instance Constructors #region Public Instance Properties public XmlSchema Schema { get { if (!_nantSchema.IsCompiled) { Compile(); } return _nantSchema; } } #endregion Public Instance Properties #region Public Instance Methods public void Compile() { _nantSchema.Compile(new ValidationEventHandler(ValidationEH)); } #endregion Public Instance Methods #region Protected Instance Methods protected XmlSchemaComplexType CreateTaskListComplexType(Type[] tasks) { return CreateTaskListComplexType(tasks, new Type[0], false); } protected XmlSchemaComplexType CreateTaskListComplexType(Type[] tasks, Type[] dataTypes, bool includeProjectLevelItems) { XmlSchemaComplexType tasklistCT = new XmlSchemaComplexType(); XmlSchemaChoice choice = new XmlSchemaChoice(); choice.MinOccurs = 0; choice.MaxOccursString = "unbounded"; tasklistCT.Particle = choice; foreach (Type t in tasks) { XmlSchemaElement taskElement = new XmlSchemaElement(); string typeId = GenerateIDFromType(t); XmlSchemaComplexType taskCT = FindComplexTypeByID(typeId); taskElement.Name = GetTaskName(t); taskElement.SchemaTypeName = taskCT.QualifiedName; choice.Items.Add(taskElement); } foreach (Type t in dataTypes) { XmlSchemaElement dataTypeElement = new XmlSchemaElement(); string typeId = GenerateIDFromType(t); XmlSchemaComplexType dataTypeCT = FindComplexTypeByID(typeId); dataTypeElement.Name = GetDataTypeName(t); dataTypeElement.SchemaTypeName = dataTypeCT.QualifiedName; choice.Items.Add(dataTypeElement); } if (includeProjectLevelItems) { XmlSchemaElement targetElement = new XmlSchemaElement(); targetElement.Name = "target"; targetElement.SchemaTypeName = _targetCT.QualifiedName; choice.Items.Add(targetElement); } // allow elements from other namespace XmlSchemaAny otherNamespaceAny = new XmlSchemaAny(); otherNamespaceAny.MinOccurs = 0; otherNamespaceAny.MaxOccurs = Decimal.MaxValue; otherNamespaceAny.Namespace = "##other"; otherNamespaceAny.ProcessContents = XmlSchemaContentProcessing.Strict; choice.Items.Add(otherNamespaceAny); // allow elements from local namespace XmlSchemaAny localNamespaceAny = new XmlSchemaAny(); localNamespaceAny.MinOccurs = 0; localNamespaceAny.MaxOccurs = Decimal.MaxValue; localNamespaceAny.Namespace = "##local"; localNamespaceAny.ProcessContents = XmlSchemaContentProcessing.Strict; choice.Items.Add(localNamespaceAny); return tasklistCT; } protected void ValidationEH(object sender, ValidationEventArgs args) { if (args.Severity == XmlSeverityType.Warning) { logger.Info("WARNING: "); } else if (args.Severity == XmlSeverityType.Error) { logger.Error("ERROR: "); } XmlSchemaComplexType source = args.Exception.SourceSchemaObject as XmlSchemaComplexType; logger.Info(args.ToString()); if (source != null) { logger.Info(string.Format(CultureInfo.InvariantCulture, "{0}", source.Name)); } } protected XmlSchemaComplexType FindComplexTypeByID(string id) { if (_nantComplexTypes.Contains(id)) { return (XmlSchemaComplexType)_nantComplexTypes[id]; } return null; } protected XmlSchemaComplexType FindOrCreateComplexType(Type t) { XmlSchemaComplexType ct; string typeId = GenerateIDFromType(t); ct = FindComplexTypeByID(typeId); if (ct != null) { return ct; } ct = new XmlSchemaComplexType(); ct.Name = typeId; // add complex type to collection immediately to avoid stack // overflows, when we allow a type to be nested _nantComplexTypes.Add(typeId, ct); #if NOT_IMPLEMENTED // // TODO - add task/type documentation in the future // ct.Annotation = new XmlSchemaAnnotation(); XmlSchemaDocumentation doc = new XmlSchemaDocumentation(); ct.Annotation.Items.Add(doc); doc.Markup = ...; #endif XmlSchemaSequence group1 = null; XmlSchemaObjectCollection attributesCollection = ct.Attributes; foreach (MemberInfo memInfo in t.GetMembers(BindingFlags.Instance | BindingFlags.Public)) { if (memInfo.DeclaringType.Equals(typeof(object))) { continue; } //Check for any return type that is derived from Element // add Attributes TaskAttributeAttribute taskAttrAttr = (TaskAttributeAttribute) Attribute.GetCustomAttribute(memInfo, typeof(TaskAttributeAttribute), false); BuildElementAttribute buildElemAttr = (BuildElementAttribute) Attribute.GetCustomAttribute(memInfo, typeof(BuildElementAttribute), false); if (taskAttrAttr != null) { XmlSchemaAttribute newAttr = CreateXsdAttribute(taskAttrAttr.Name, taskAttrAttr.Required); attributesCollection.Add(newAttr); } else if (buildElemAttr != null) { // Create individial choice for any individual child Element Decimal min = 0; if (buildElemAttr.Required) { min = 1; } XmlSchemaElement childElement = new XmlSchemaElement(); childElement.MinOccurs = min; childElement.MaxOccurs = 1; childElement.Name = buildElemAttr.Name; //XmlSchemaGroupBase elementGroup = CreateXsdSequence(min, Decimal.MaxValue); Type childType; // We will only process child elements if they are defined for Properties or Fields, this should be enforced by the AttributeUsage on the Attribute class if (memInfo is PropertyInfo) { childType = ((PropertyInfo) memInfo).PropertyType; } else if (memInfo is FieldInfo) { childType = ((FieldInfo) memInfo).FieldType; } else if (memInfo is MethodInfo) { MethodInfo method = (MethodInfo) memInfo; if (method.GetParameters().Length == 1) { childType = method.GetParameters()[0].ParameterType; } else { throw new ApplicationException("Method should have one parameter."); } } else { throw new ApplicationException("Member Type != Field/Property/Method"); } BuildElementArrayAttribute buildElementArrayAttribute = (BuildElementArrayAttribute) Attribute.GetCustomAttribute(memInfo, typeof(BuildElementArrayAttribute), false); // determine type of child elements if (buildElementArrayAttribute != null) { if (buildElementArrayAttribute.ElementType == null) { if (childType.IsArray) { childType = childType.GetElementType(); } else { Type elementType = null; // locate Add method with 1 parameter, type of that parameter is parameter type foreach (MethodInfo method in childType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (method.Name == "Add" && method.GetParameters().Length == 1) { ParameterInfo parameter = method.GetParameters()[0]; elementType = parameter.ParameterType; break; } } childType = elementType; } } else { childType = buildElementArrayAttribute.ElementType; } if (childType == null || !typeof(Element).IsAssignableFrom(childType)) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1140"), memInfo.DeclaringType.FullName, memInfo.Name)); } } BuildElementCollectionAttribute buildElementCollectionAttribute = (BuildElementCollectionAttribute) Attribute.GetCustomAttribute(memInfo, typeof(BuildElementCollectionAttribute), false); if (buildElementCollectionAttribute != null) { XmlSchemaComplexType collectionType = new XmlSchemaComplexType(); XmlSchemaSequence sequence = new XmlSchemaSequence(); collectionType.Particle = sequence; sequence.MinOccurs = 0; sequence.MaxOccursString = "unbounded"; XmlSchemaElement itemType = new XmlSchemaElement(); itemType.Name = buildElementCollectionAttribute.ChildElementName; itemType.SchemaTypeName = FindOrCreateComplexType(childType).QualifiedName; sequence.Items.Add(itemType); childElement.SchemaType = collectionType; } else { childElement.SchemaTypeName = FindOrCreateComplexType(childType).QualifiedName; } // lazy init of sequence if (group1 == null) { group1 = CreateXsdSequence(0, Decimal.MaxValue); ct.Particle = group1; } group1.Items.Add(childElement); } } // allow attributes from other namespace ct.AnyAttribute = new XmlSchemaAnyAttribute(); ct.AnyAttribute.Namespace = "##other"; ct.AnyAttribute.ProcessContents = XmlSchemaContentProcessing.Skip; Schema.Items.Add(ct); Compile(); return ct; } #endregion Protected Instance Methods #region Private Instance Methods private string GetTaskName(Type t) { TaskNameAttribute[] attrs = (TaskNameAttribute[])t.GetCustomAttributes(typeof(TaskNameAttribute), false); if (attrs.Length == 1) { return attrs[0].Name; } else { return null; } } private string GetDataTypeName(Type t) { ElementNameAttribute[] attrs = (ElementNameAttribute[]) t.GetCustomAttributes(typeof(ElementNameAttribute), false); if (attrs.Length == 1) { return attrs[0].Name; } else { return null; } } #endregion Private Instance Methods } } }