1 //
2 // AddinDescription.cs
3 //
4 // Author:
5 //   Lluis Sanchez Gual
6 //
7 // Copyright (C) 2007 Novell, Inc (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28 
29 using System;
30 using System.Collections;
31 using System.Collections.Generic;
32 using System.IO;
33 using System.Xml;
34 using System.Xml.Serialization;
35 using System.Collections.Specialized;
36 using Mono.Addins.Serialization;
37 using Mono.Addins.Database;
38 using System.Text;
39 
40 namespace Mono.Addins.Description
41 {
42 	/// <summary>
43 	/// An add-in description
44 	/// </summary>
45 	/// <remarks>
46 	/// This class represent an add-in manifest. It has properties for getting
47 	/// all information, and methods for loading and saving files.
48 	///	</remarks>
49 	public class AddinDescription: IBinaryXmlElement
50 	{
51 		XmlDocument configDoc;
52 		string configFile;
53 		AddinDatabase ownerDatabase;
54 
55 		string id;
56 		string name;
57 		string ns;
58 		string version;
59 		string compatVersion;
60 		string author;
61 		string url;
62 		string copyright;
63 		string description;
64 		string category;
65 		string basePath;
66 		string sourceAddinFile;
67 		bool isroot;
68 		bool hasUserId;
69 		bool canWrite = true;
70 		bool defaultEnabled = true;
71 		AddinFlags flags = AddinFlags.None;
72 		string domain;
73 
74 		ModuleDescription mainModule;
75 		ModuleCollection optionalModules;
76 		ExtensionNodeSetCollection nodeSets;
77 		ConditionTypeDescriptionCollection conditionTypes;
78 		ExtensionPointCollection extensionPoints;
79 		ExtensionNodeDescription localizer;
80 		object[] fileInfo;
81 
82 		AddinPropertyCollectionImpl properties;
83 		Dictionary<string,string> variables;
84 
85 		internal static BinaryXmlTypeMap typeMap;
86 
AddinDescription()87 		static AddinDescription ()
88 		{
89 			typeMap = new BinaryXmlTypeMap ();
90 			typeMap.RegisterType (typeof(AddinDescription), "AddinDescription");
91 			typeMap.RegisterType (typeof(Extension), "Extension");
92 			typeMap.RegisterType (typeof(ExtensionNodeDescription), "Node");
93 			typeMap.RegisterType (typeof(ExtensionNodeSet), "NodeSet");
94 			typeMap.RegisterType (typeof(ExtensionNodeType), "NodeType");
95 			typeMap.RegisterType (typeof(ExtensionPoint), "ExtensionPoint");
96 			typeMap.RegisterType (typeof(ModuleDescription), "ModuleDescription");
97 			typeMap.RegisterType (typeof(ConditionTypeDescription), "ConditionType");
98 			typeMap.RegisterType (typeof(Condition), "Condition");
99 			typeMap.RegisterType (typeof(AddinDependency), "AddinDependency");
100 			typeMap.RegisterType (typeof(AssemblyDependency), "AssemblyDependency");
101 			typeMap.RegisterType (typeof(NodeTypeAttribute), "NodeTypeAttribute");
102 			typeMap.RegisterType (typeof(AddinFileInfo), "FileInfo");
103 			typeMap.RegisterType (typeof(AddinProperty), "Property");
104 		}
105 
106 		internal AddinDatabase OwnerDatabase {
107 			get { return ownerDatabase; }
108 			set { ownerDatabase = value; }
109 		}
110 
111 		/// <summary>
112 		/// Gets or sets the path to the main addin file.
113 		/// </summary>
114 		/// <value>
115 		/// The addin file.
116 		/// </value>
117 		/// <remarks>
118 		/// The add-in file can be either the main assembly of an add-in or an xml manifest.
119 		/// </remarks>
120 		public string AddinFile {
121 			get { return sourceAddinFile; }
122 			set { sourceAddinFile = value; }
123 		}
124 
125 		/// <summary>
126 		/// Gets the addin identifier.
127 		/// </summary>
128 		/// <value>
129 		/// The addin identifier.
130 		/// </value>
131 		public string AddinId {
132 			get { return Addin.GetFullId (Namespace, LocalId, Version); }
133 		}
134 
135 		/// <summary>
136 		/// Gets or sets the local identifier.
137 		/// </summary>
138 		/// <value>
139 		/// The local identifier.
140 		/// </value>
141 		public string LocalId {
142 			get { return id != null ? ParseString (id) : string.Empty; }
143 			set { id = value; hasUserId = true; }
144 		}
145 
146 		/// <summary>
147 		/// Gets or sets the namespace.
148 		/// </summary>
149 		/// <value>
150 		/// The namespace.
151 		/// </value>
152 		public string Namespace {
153 			get { return ns != null ? ParseString (ns) : string.Empty; }
154 			set { ns = value; }
155 		}
156 
157 		/// <summary>
158 		/// Gets or sets the display name of the add-in.
159 		/// </summary>
160 		/// <value>
161 		/// The name.
162 		/// </value>
163 		public string Name {
164 			get {
165 				string val = Properties.GetPropertyValue ("Name");
166 				if (val.Length > 0)
167 					return val;
168 				if (name != null && name.Length > 0)
169 					return ParseString (name);
170 				if (HasUserId)
171 					return AddinId;
172 				else if (sourceAddinFile != null)
173 					return Path.GetFileNameWithoutExtension (sourceAddinFile);
174 				else
175 					return string.Empty;
176 			}
177 			set { name = value; }
178 		}
179 
180 		/// <summary>
181 		/// Gets or sets the version.
182 		/// </summary>
183 		/// <value>
184 		/// The version.
185 		/// </value>
186 		public string Version {
187 			get { return version != null ? ParseString (version) : string.Empty; }
188 			set { version = value; }
189 		}
190 
191 		/// <summary>
192 		/// Gets or sets the version of the add-in with which this add-in is backwards compatible.
193 		/// </summary>
194 		/// <value>
195 		/// The compat version.
196 		/// </value>
197 		public string CompatVersion {
198 			get { return compatVersion != null ? ParseString (compatVersion) : string.Empty; }
199 			set { compatVersion = value; }
200 		}
201 
202 		/// <summary>
203 		/// Gets or sets the author.
204 		/// </summary>
205 		/// <value>
206 		/// The author.
207 		/// </value>
208 		public string Author {
209 			get {
210 				string val = Properties.GetPropertyValue ("Author");
211 				if (val.Length > 0)
212 					return val;
213 				return ParseString (author) ?? string.Empty;
214 			}
215 			set { author = value; }
216 		}
217 
218 		/// <summary>
219 		/// Gets or sets the Url where more information about the add-in can be found.
220 		/// </summary>
221 		/// <value>
222 		/// The URL.
223 		/// </value>
224 		public string Url {
225 			get {
226 				string val = Properties.GetPropertyValue ("Url");
227 				if (val.Length > 0)
228 					return val;
229 				return ParseString (url) ?? string.Empty;
230 			}
231 			set { url = value; }
232 		}
233 
234 		/// <summary>
235 		/// Gets or sets the copyright.
236 		/// </summary>
237 		/// <value>
238 		/// The copyright.
239 		/// </value>
240 		public string Copyright {
241 			get {
242 				string val = Properties.GetPropertyValue ("Copyright");
243 				if (val.Length > 0)
244 					return val;
245 				return ParseString (copyright) ?? string.Empty;
246 			}
247 			set { copyright = value; }
248 		}
249 
250 		/// <summary>
251 		/// Gets or sets the description of the add-in.
252 		/// </summary>
253 		/// <value>
254 		/// The description.
255 		/// </value>
256 		public string Description {
257 			get {
258 				string val = Properties.GetPropertyValue ("Description");
259 				if (val.Length > 0)
260 					return val;
261 				return ParseString (description) ?? string.Empty;
262 			}
263 			set { description = value; }
264 		}
265 
266 		/// <summary>
267 		/// Gets or sets the category of the add-in.
268 		/// </summary>
269 		/// <value>
270 		/// The category.
271 		/// </value>
272 		public string Category {
273 			get {
274 				string val = Properties.GetPropertyValue ("Category");
275 				if (val.Length > 0)
276 					return val;
277 				return ParseString (category) ?? string.Empty;
278 			}
279 			set { category = value; }
280 		}
281 
282 		/// <summary>
283 		/// Gets the base path for locating external files relative to the add-in.
284 		/// </summary>
285 		/// <value>
286 		/// The base path.
287 		/// </value>
288 		public string BasePath {
289 			get { return basePath != null ? basePath : string.Empty; }
290 		}
291 
SetBasePath(string path)292 		internal void SetBasePath (string path)
293 		{
294 			basePath = path;
295 		}
296 
297 		/// <summary>
298 		/// Gets or sets a value indicating whether this instance is an add-in root.
299 		/// </summary>
300 		/// <value>
301 		/// <c>true</c> if this instance is an add-in root; otherwise, <c>false</c>.
302 		/// </value>
303 		public bool IsRoot {
304 			get { return isroot; }
305 			set { isroot = value; }
306 		}
307 
308 		/// <summary>
309 		/// Gets or sets a value indicating whether this add-in is enabled by default.
310 		/// </summary>
311 		/// <value>
312 		/// <c>true</c> if enabled by default; otherwise, <c>false</c>.
313 		/// </value>
314 		public bool EnabledByDefault {
315 			get { return defaultEnabled; }
316 			set { defaultEnabled = value; }
317 		}
318 
319 		/// <summary>
320 		/// Gets or sets the add-in flags.
321 		/// </summary>
322 		/// <value>
323 		/// The flags.
324 		/// </value>
325 		public AddinFlags Flags {
326 			get { return flags; }
327 			set { flags = value; }
328 		}
329 
330 		internal bool HasUserId {
331 			get { return hasUserId; }
332 			set { hasUserId = value; }
333 		}
334 
335 		/// <summary>
336 		/// Gets a value indicating whether this add-in can be disabled.
337 		/// </summary>
338 		/// <value>
339 		/// <c>true</c> if this add-in can be disabled; otherwise, <c>false</c>.
340 		/// </value>
341 		public bool CanDisable {
342 			get { return (flags & AddinFlags.CantDisable) == 0 && !IsHidden; }
343 		}
344 
345 		/// <summary>
346 		/// Gets a value indicating whether this add-in can be uninstalled.
347 		/// </summary>
348 		/// <value>
349 		/// <c>true</c> if this instance can be uninstalled; otherwise, <c>false</c>.
350 		/// </value>
351 		public bool CanUninstall {
352 			get { return (flags & AddinFlags.CantUninstall) == 0 && !IsHidden; }
353 		}
354 
355 		/// <summary>
356 		/// Gets a value indicating whether this add-in is hidden.
357 		/// </summary>
358 		/// <value>
359 		/// <c>true</c> if this add-in is hidden; otherwise, <c>false</c>.
360 		/// </value>
361 		public bool IsHidden {
362 			get { return (flags & AddinFlags.Hidden) != 0; }
363 		}
364 
SupportsVersion(string ver)365 		internal bool SupportsVersion (string ver)
366 		{
367 			return Addin.CompareVersions (ver, Version) >= 0 &&
368 				   (CompatVersion.Length == 0 || Addin.CompareVersions (ver, CompatVersion) <= 0);
369 		}
370 
371 		/// <summary>
372 		/// Gets all external files
373 		/// </summary>
374 		/// <value>
375 		/// All files.
376 		/// </value>
377 		/// <remarks>
378 		/// External files are data files and assemblies explicitly referenced in the Runtime section of the add-in manifest.
379 		/// </remarks>
380 		public StringCollection AllFiles {
381 			get {
382 				StringCollection col = new StringCollection ();
383 				foreach (string s in MainModule.AllFiles)
384 					col.Add (s);
385 
386 				foreach (ModuleDescription mod in OptionalModules) {
387 					foreach (string s in mod.AllFiles)
388 						col.Add (s);
389 				}
390 				return col;
391 			}
392 		}
393 
394 		/// <summary>
395 		/// Gets all paths to be ignored by the add-in scanner.
396 		/// </summary>
397 		/// <value>
398 		/// All paths to be ignored.
399 		/// </value>
400 		public StringCollection AllIgnorePaths {
401 			get {
402 				StringCollection col = new StringCollection ();
403 				foreach (string s in MainModule.IgnorePaths)
404 					col.Add (s);
405 
406 				foreach (ModuleDescription mod in OptionalModules) {
407 					foreach (string s in mod.IgnorePaths)
408 						col.Add (s);
409 				}
410 				return col;
411 			}
412 		}
413 
414 		/// <summary>
415 		/// Gets the main module.
416 		/// </summary>
417 		/// <value>
418 		/// The main module.
419 		/// </value>
420 		public ModuleDescription MainModule {
421 			get {
422 				if (mainModule == null) {
423 					if (RootElement == null)
424 						mainModule = new ModuleDescription ();
425 					else
426 						mainModule = new ModuleDescription (RootElement);
427 					mainModule.SetParent (this);
428 				}
429 				return mainModule;
430 			}
431 		}
432 
433 		/// <summary>
434 		/// Gets the optional modules.
435 		/// </summary>
436 		/// <value>
437 		/// The optional modules.
438 		/// </value>
439 		/// <remarks>
440 		/// Optional modules can be used to declare extensions which will be registered only if some specified
441 		/// add-in dependencies can be satisfied. Dependencies specified in optional modules are 'soft dependencies',
442 		/// which means that they don't need to be satisfied in order to load the add-in.
443 		/// </remarks>
444 		public ModuleCollection OptionalModules {
445 			get {
446 				if (optionalModules == null) {
447 					optionalModules = new ModuleCollection (this);
448 					if (RootElement != null) {
449 						foreach (XmlElement mod in RootElement.SelectNodes ("Module"))
450 							optionalModules.Add (new ModuleDescription (mod));
451 					}
452 				}
453 				return optionalModules;
454 			}
455 		}
456 
457 		/// <summary>
458 		/// Gets all modules (including the main module and all optional modules)
459 		/// </summary>
460 		/// <value>
461 		/// All modules.
462 		/// </value>
463 		public ModuleCollection AllModules {
464 			get {
465 				ModuleCollection col = new ModuleCollection (this);
466 				col.Add (MainModule);
467 				foreach (ModuleDescription mod in OptionalModules)
468 					col.Add (mod);
469 				return col;
470 			}
471 		}
472 
473 		/// <summary>
474 		/// Gets the extension node sets.
475 		/// </summary>
476 		/// <value>
477 		/// The extension node sets.
478 		/// </value>
479 		public ExtensionNodeSetCollection ExtensionNodeSets {
480 			get {
481 				if (nodeSets == null) {
482 					nodeSets = new ExtensionNodeSetCollection (this);
483 					if (RootElement != null) {
484 						foreach (XmlElement elem in RootElement.SelectNodes ("ExtensionNodeSet"))
485 							nodeSets.Add (new ExtensionNodeSet (elem));
486 					}
487 				}
488 				return nodeSets;
489 			}
490 		}
491 
492 		/// <summary>
493 		/// Gets the extension points.
494 		/// </summary>
495 		/// <value>
496 		/// The extension points.
497 		/// </value>
498 		public ExtensionPointCollection ExtensionPoints {
499 			get {
500 				if (extensionPoints == null) {
501 					extensionPoints = new ExtensionPointCollection (this);
502 					if (RootElement != null) {
503 						foreach (XmlElement elem in RootElement.SelectNodes ("ExtensionPoint"))
504 							extensionPoints.Add (new ExtensionPoint (elem));
505 					}
506 				}
507 				return extensionPoints;
508 			}
509 		}
510 
511 		/// <summary>
512 		/// Gets the condition types.
513 		/// </summary>
514 		/// <value>
515 		/// The condition types.
516 		/// </value>
517 		public ConditionTypeDescriptionCollection ConditionTypes {
518 			get {
519 				if (conditionTypes == null) {
520 					conditionTypes = new ConditionTypeDescriptionCollection (this);
521 					if (RootElement != null) {
522 						foreach (XmlElement elem in RootElement.SelectNodes ("ConditionType"))
523 							conditionTypes.Add (new ConditionTypeDescription (elem));
524 					}
525 				}
526 				return conditionTypes;
527 			}
528 		}
529 
530 		/// <summary>
531 		/// Gets or sets the add-in localizer.
532 		/// </summary>
533 		/// <value>
534 		/// The description of the add-in localizer for this add-in.
535 		/// </value>
536 		public ExtensionNodeDescription Localizer {
537 			get { return localizer; }
538 			set { localizer = value; }
539 		}
540 
541 		/// <summary>
542 		/// Custom properties specified in the add-in header
543 		/// </summary>
544 		public AddinPropertyCollection Properties {
545 			get {
546 				if (properties == null)
547 					properties = new AddinPropertyCollectionImpl (this);
548 				return properties;
549 			}
550 		}
551 
552 		/// <summary>
553 		/// Adds an extension point.
554 		/// </summary>
555 		/// <returns>
556 		/// The extension point.
557 		/// </returns>
558 		/// <param name='path'>
559 		/// Path that identifies the new extension point.
560 		/// </param>
AddExtensionPoint(string path)561 		public ExtensionPoint AddExtensionPoint (string path)
562 		{
563 			ExtensionPoint ep = new ExtensionPoint ();
564 			ep.Path = path;
565 			ExtensionPoints.Add (ep);
566 			return ep;
567 		}
568 
FindExtensionNode(string path, bool lookInDeps)569 		internal ExtensionNodeDescription FindExtensionNode (string path, bool lookInDeps)
570 		{
571 			// Look in the extensions of this add-in
572 
573 			foreach (Extension ext in MainModule.Extensions) {
574 				if (path.StartsWith (ext.Path + "/")) {
575 					string subp = path.Substring (ext.Path.Length).Trim ('/');
576 					ExtensionNodeDescriptionCollection nodes = ext.ExtensionNodes;
577 					ExtensionNodeDescription node = null;
578 					foreach (string p in subp.Split ('/')) {
579 						if (p.Length == 0) continue;
580 						node = nodes [p];
581 						if (node == null)
582 							break;
583 						nodes = node.ChildNodes;
584 					}
585 					if (node != null)
586 						return node;
587 				}
588 			}
589 
590 			if (!lookInDeps || OwnerDatabase == null)
591 				return null;
592 
593 			// Look in dependencies
594 
595 			foreach (Dependency dep in MainModule.Dependencies) {
596 				AddinDependency adep = dep as AddinDependency;
597 				if (adep == null) continue;
598 				Addin ad = OwnerDatabase.GetInstalledAddin (Domain, adep.FullAddinId);
599 				if (ad != null && ad.Description != null) {
600 					ExtensionNodeDescription node = ad.Description.FindExtensionNode (path, false);
601 					if (node != null)
602 						return node;
603 				}
604 			}
605 			return null;
606 		}
607 
608 		XmlElement RootElement {
609 			get {
610 				if (configDoc != null)
611 					return configDoc.DocumentElement;
612 				else
613 					return null;
614 			}
615 		}
616 
ResetXmlDoc()617 		internal void ResetXmlDoc ()
618 		{
619 			configDoc = null;
620 		}
621 
622 		/// <summary>
623 		/// Gets or sets file where this description is stored
624 		/// </summary>
625 		/// <value>
626 		/// The file path.
627 		/// </value>
628 		public string FileName {
629 			get { return configFile; }
630 			set { configFile = value; }
631 		}
632 
633 		internal string Domain {
634 			get { return domain; }
635 			set { domain = value; }
636 		}
637 
StoreFileInfo()638 		internal void StoreFileInfo ()
639 		{
640 			ArrayList list = new ArrayList ();
641 			foreach (string f in AllFiles) {
642 				string file = Path.Combine (this.BasePath, f);
643 				AddinFileInfo fi = new AddinFileInfo ();
644 				fi.FileName = f;
645 				fi.Timestamp = File.GetLastWriteTime (file);
646 				list.Add (fi);
647 			}
648 			fileInfo = list.ToArray ();
649 		}
650 
FilesChanged()651 		internal bool FilesChanged ()
652 		{
653 			// Checks if the files of the add-in have changed.
654 			if (fileInfo == null)
655 				return true;
656 
657 			foreach (AddinFileInfo f in fileInfo) {
658 				string file = Path.Combine (this.BasePath, f.FileName);
659 				if (!File.Exists (file))
660 					return true;
661 				if (f.Timestamp != File.GetLastWriteTime (file))
662 					return true;
663 			}
664 
665 			return false;
666 		}
667 
TransferCoreProperties(bool removeProperties)668 		void TransferCoreProperties (bool removeProperties)
669 		{
670 			if (properties == null)
671 				return;
672 
673 			string val = properties.ExtractCoreProperty ("Id", removeProperties);
674 			if (val != null)
675 				id = val;
676 
677 			val = properties.ExtractCoreProperty ("Namespace", removeProperties);
678 			if (val != null)
679 				ns = val;
680 
681 			val = properties.ExtractCoreProperty ("Version", removeProperties);
682 			if (val != null)
683 				version = val;
684 
685 			val = properties.ExtractCoreProperty ("CompatVersion", removeProperties);
686 			if (val != null)
687 				compatVersion = val;
688 
689 			val = properties.ExtractCoreProperty ("DefaultEnabled", removeProperties);
690 			if (val != null)
691 				defaultEnabled = GetBool (val, true);
692 
693 			val = properties.ExtractCoreProperty ("IsRoot", removeProperties);
694 			if (val != null)
695 				isroot = GetBool (val, true);
696 
697 			val = properties.ExtractCoreProperty ("Flags", removeProperties);
698 			if (val != null)
699 				flags = (AddinFlags) Enum.Parse (typeof(AddinFlags), val);
700 		}
701 
702 		/// <summary>
703 		/// Saves the add-in description.
704 		/// </summary>
705 		/// <param name='fileName'>
706 		/// File name where to save this instance
707 		/// </param>
708 		/// <remarks>
709 		/// Saves the add-in description to the specified file and sets the FileName property.
710 		/// </remarks>
Save(string fileName)711 		public void Save (string fileName)
712 		{
713 			configFile = fileName;
714 			Save ();
715 		}
716 
717 		/// <summary>
718 		/// Saves the add-in description.
719 		/// </summary>
720 		/// <exception cref='InvalidOperationException'>
721 		/// It is thrown if FileName is not set
722 		/// </exception>
723 		/// <remarks>
724 		/// The description is saved to the file specified in the FileName property.
725 		/// </remarks>
Save()726 		public void Save ()
727 		{
728 			if (configFile == null)
729 				throw new InvalidOperationException ("File name not specified.");
730 
731 			SaveXml ();
732 
733 			using (StreamWriter sw = new StreamWriter (configFile)) {
734 				XmlTextWriter tw = new XmlTextWriter (sw);
735 				tw.Formatting = Formatting.Indented;
736 				configDoc.Save (tw);
737 			}
738 		}
739 
740 		/// <summary>
741 		/// Generates an XML representation of the add-in description
742 		/// </summary>
743 		/// <returns>
744 		/// An XML manifest.
745 		/// </returns>
SaveToXml()746 		public XmlDocument SaveToXml ()
747 		{
748 			SaveXml ();
749 			return configDoc;
750 		}
751 
SaveXml()752 		void SaveXml ()
753 		{
754 			if (!canWrite)
755 				throw new InvalidOperationException ("Can't write incomplete description.");
756 
757 			XmlElement elem;
758 
759 			if (configDoc == null) {
760 				configDoc = new XmlDocument ();
761 				configDoc.AppendChild (configDoc.CreateElement ("Addin"));
762 			}
763 
764 			elem = configDoc.DocumentElement;
765 
766 			SaveCoreProperty (elem, HasUserId ? id : null, "id", "Id");
767 			SaveCoreProperty (elem, version, "version", "Version");
768 			SaveCoreProperty (elem, ns, "namespace", "Namespace");
769 			SaveCoreProperty (elem, isroot ? "true" : null, "isroot", "IsRoot");
770 
771 			// Name will return the file name when HasUserId=false
772 			if (!string.IsNullOrEmpty (name))
773 				elem.SetAttribute ("name", name);
774 			else
775 				elem.RemoveAttribute ("name");
776 
777 			SaveCoreProperty (elem, compatVersion, "compatVersion", "CompatVersion");
778 			SaveCoreProperty (elem, defaultEnabled ? null : "false", "defaultEnabled", "DefaultEnabled");
779 			SaveCoreProperty (elem, flags != AddinFlags.None ? flags.ToString () : null, "flags", "Flags");
780 
781 			if (author != null && author.Length > 0)
782 				elem.SetAttribute ("author", author);
783 			else
784 				elem.RemoveAttribute ("author");
785 
786 			if (url != null && url.Length > 0)
787 				elem.SetAttribute ("url", url);
788 			else
789 				elem.RemoveAttribute ("url");
790 
791 			if (copyright != null && copyright.Length > 0)
792 				elem.SetAttribute ("copyright", copyright);
793 			else
794 				elem.RemoveAttribute ("copyright");
795 
796 			if (description != null && description.Length > 0)
797 				elem.SetAttribute ("description", description);
798 			else
799 				elem.RemoveAttribute ("description");
800 
801 			if (category != null && category.Length > 0)
802 				elem.SetAttribute ("category", category);
803 			else
804 				elem.RemoveAttribute ("category");
805 
806 			if (localizer == null || localizer.Element == null) {
807 				// Remove old element if it exists
808 				XmlElement oldLoc = (XmlElement) elem.SelectSingleNode ("Localizer");
809 				if (oldLoc != null)
810 					elem.RemoveChild (oldLoc);
811 			}
812 			if (localizer != null)
813 				localizer.SaveXml (elem);
814 
815 			if (mainModule != null) {
816 				mainModule.Element = elem;
817 				mainModule.SaveXml (elem);
818 			}
819 
820 			if (optionalModules != null)
821 				optionalModules.SaveXml (elem);
822 
823 			if (nodeSets != null)
824 				nodeSets.SaveXml (elem);
825 
826 			if (extensionPoints != null)
827 				extensionPoints.SaveXml (elem);
828 
829 			XmlElement oldHeader = (XmlElement) elem.SelectSingleNode ("Header");
830 			if (properties == null || properties.Count == 0) {
831 				if (oldHeader != null)
832 					elem.RemoveChild (oldHeader);
833 			} else {
834 				if (oldHeader == null) {
835 					oldHeader = elem.OwnerDocument.CreateElement ("Header");
836 					if (elem.FirstChild != null)
837 						elem.InsertBefore (oldHeader, elem.FirstChild);
838 					else
839 						elem.AppendChild (oldHeader);
840 				}
841 				else
842 					oldHeader.RemoveAll ();
843 				foreach (var prop in properties) {
844 					XmlElement propElem = elem.OwnerDocument.CreateElement (prop.Name);
845 					if (!string.IsNullOrEmpty (prop.Locale))
846 						propElem.SetAttribute ("locale", prop.Locale);
847 					propElem.InnerText = prop.Value ?? string.Empty;
848 					oldHeader.AppendChild (propElem);
849 				}
850 			}
851 
852 			XmlElement oldVars = (XmlElement) elem.SelectSingleNode ("Variables");
853 			if (variables == null || variables.Count == 0) {
854 				if (oldVars != null)
855 					elem.RemoveChild (oldVars);
856 			} else {
857 				if (oldVars == null) {
858 					oldVars = elem.OwnerDocument.CreateElement ("Variables");
859 					if (elem.FirstChild != null)
860 						elem.InsertBefore (oldVars, elem.FirstChild);
861 					else
862 						elem.AppendChild (oldVars);
863 				}
864 				else
865 					oldVars.RemoveAll ();
866 				foreach (var prop in variables) {
867 					XmlElement propElem = elem.OwnerDocument.CreateElement (prop.Key);
868 					propElem.InnerText = prop.Value ?? string.Empty;
869 					oldVars.AppendChild (propElem);
870 				}
871 			}
872 		}
873 
SaveCoreProperty(XmlElement elem, string val, string attr, string prop)874 		void SaveCoreProperty (XmlElement elem, string val, string attr, string prop)
875 		{
876 			if (properties != null && properties.HasProperty (prop)) {
877 				elem.RemoveAttribute (attr);
878 				if (!string.IsNullOrEmpty (val))
879 					properties.SetPropertyValue (prop, val);
880 				else
881 					properties.RemoveProperty (prop);
882 			}
883 			else if (string.IsNullOrEmpty (val))
884 				elem.RemoveAttribute (attr);
885 			else
886 				elem.SetAttribute (attr, val);
887 		}
888 
889 
890 		/// <summary>
891 		/// Load an add-in description from a file
892 		/// </summary>
893 		/// <param name='configFile'>
894 		/// The file.
895 		/// </param>
Read(string configFile)896 		public static AddinDescription Read (string configFile)
897 		{
898 			AddinDescription config;
899 			using (Stream s = File.OpenRead (configFile)) {
900 				config = Read (s, Path.GetDirectoryName (configFile));
901 			}
902 			config.configFile = configFile;
903 			return config;
904 		}
905 
906 		/// <summary>
907 		/// Load an add-in description from a stream
908 		/// </summary>
909 		/// <param name='stream'>
910 		/// The stream
911 		/// </param>
912 		/// <param name='basePath'>
913 		/// The path to be used to resolve relative file paths.
914 		/// </param>
Read(Stream stream, string basePath)915 		public static AddinDescription Read (Stream stream, string basePath)
916 		{
917 			return Read (new StreamReader (stream), basePath);
918 		}
919 
920 		/// <summary>
921 		/// Load an add-in description from a text reader
922 		/// </summary>
923 		/// <param name='reader'>
924 		/// The text reader
925 		/// </param>
926 		/// <param name='basePath'>
927 		/// The path to be used to resolve relative file paths.
928 		/// </param>
Read(TextReader reader, string basePath)929 		public static AddinDescription Read (TextReader reader, string basePath)
930 		{
931 			AddinDescription config = new AddinDescription ();
932 
933 			try {
934 				config.configDoc = new XmlDocument ();
935 				config.configDoc.Load (reader);
936 			} catch (Exception ex) {
937 				throw new InvalidOperationException ("The add-in configuration file is invalid: " + ex.Message, ex);
938 			}
939 
940 			XmlElement elem = config.configDoc.DocumentElement;
941 			if (elem.LocalName == "ExtensionModel")
942 				return config;
943 
944 			XmlElement varsElem = (XmlElement) elem.SelectSingleNode ("Variables");
945 			if (varsElem != null) {
946 				foreach (XmlNode node in varsElem.ChildNodes) {
947 					XmlElement prop = node as XmlElement;
948 					if (prop == null)
949 						continue;
950 					if (config.variables == null)
951 						config.variables = new Dictionary<string, string> ();
952 					config.variables [prop.LocalName] = prop.InnerText;
953 				}
954 			}
955 
956 			config.id = elem.GetAttribute ("id");
957 			config.ns = elem.GetAttribute ("namespace");
958 			config.name = elem.GetAttribute ("name");
959 			config.version = elem.GetAttribute ("version");
960 			config.compatVersion = elem.GetAttribute ("compatVersion");
961 			config.author = elem.GetAttribute ("author");
962 			config.url = elem.GetAttribute ("url");
963 			config.copyright = elem.GetAttribute ("copyright");
964 			config.description = elem.GetAttribute ("description");
965 			config.category = elem.GetAttribute ("category");
966 			config.basePath = elem.GetAttribute ("basePath");
967 			config.domain = "global";
968 
969 			string s = elem.GetAttribute ("isRoot");
970 			if (s.Length == 0) s = elem.GetAttribute ("isroot");
971 			config.isroot = GetBool (s, false);
972 
973 			config.defaultEnabled = GetBool (elem.GetAttribute ("defaultEnabled"), true);
974 
975 			string prot = elem.GetAttribute ("flags");
976 			if (prot.Length == 0)
977 				config.flags = AddinFlags.None;
978 			else
979 				config.flags = (AddinFlags) Enum.Parse (typeof(AddinFlags), prot);
980 
981 			XmlElement localizerElem = (XmlElement) elem.SelectSingleNode ("Localizer");
982 			if (localizerElem != null)
983 				config.localizer = new ExtensionNodeDescription (localizerElem);
984 
985 			XmlElement headerElem = (XmlElement) elem.SelectSingleNode ("Header");
986 			if (headerElem != null) {
987 				foreach (XmlNode node in headerElem.ChildNodes) {
988 					XmlElement prop = node as XmlElement;
989 					if (prop == null)
990 						continue;
991 						config.Properties.SetPropertyValue (prop.LocalName, prop.InnerText, prop.GetAttribute ("locale"));
992 				}
993 			}
994 
995 			config.TransferCoreProperties (false);
996 
997 			if (config.id.Length > 0)
998 				config.hasUserId = true;
999 
1000 			return config;
1001 		}
1002 
ParseString(string input)1003 		internal string ParseString (string input)
1004 		{
1005 			if (input == null || input.Length < 4 || variables == null || variables.Count == 0)
1006 				return input;
1007 
1008 			int i = input.IndexOf ("$(");
1009 			if (i == -1)
1010 				return input;
1011 
1012 			StringBuilder result = new StringBuilder (input.Length);
1013 			result.Append (input, 0, i);
1014 
1015 			while (i < input.Length) {
1016 				if (input [i] == '$') {
1017 					i++;
1018 
1019 					if (i >= input.Length || input[i] != '(') {
1020 						result.Append ('$');
1021 						continue;
1022 					}
1023 
1024 					i++;
1025 					int start = i;
1026 					while (i < input.Length && input [i] != ')')
1027 						i++;
1028 
1029 					string tag = input.Substring (start, i - start);
1030 
1031 					string tagValue;
1032 					if (variables.TryGetValue (tag, out tagValue))
1033 						result.Append (tagValue);
1034 					else {
1035 						result.Append ('$');
1036 						i = start - 1;
1037 					}
1038 				} else {
1039 					result.Append (input [i]);
1040 				}
1041 				i++;
1042 			}
1043 			return result.ToString ();
1044 		}
1045 
GetBool(string s, bool defval)1046 		static bool GetBool (string s, bool defval)
1047 		{
1048 			if (s.Length == 0)
1049 				return defval;
1050 			else
1051 				return s == "true" || s == "yes";
1052 		}
1053 
ReadBinary(FileDatabase fdb, string configFile)1054 		internal static AddinDescription ReadBinary (FileDatabase fdb, string configFile)
1055 		{
1056 			AddinDescription description = (AddinDescription) fdb.ReadSharedObject (configFile, typeMap);
1057 			if (description != null) {
1058 				description.FileName = configFile;
1059 				description.canWrite = !fdb.IgnoreDescriptionData;
1060 			}
1061 			return description;
1062 		}
1063 
SaveBinary(FileDatabase fdb, string file)1064 		internal void SaveBinary (FileDatabase fdb, string file)
1065 		{
1066 			configFile = file;
1067 			SaveBinary (fdb);
1068 		}
1069 
SaveBinary(FileDatabase fdb)1070 		internal void SaveBinary (FileDatabase fdb)
1071 		{
1072 			if (!canWrite)
1073 				throw new InvalidOperationException ("Can't write incomplete description.");
1074 			fdb.WriteSharedObject (AddinFile, FileName, typeMap, this);
1075 //			BinaryXmlReader.DumpFile (configFile);
1076 		}
1077 
1078 		/// <summary>
1079 		/// Verify this instance.
1080 		/// </summary>
1081 		/// <remarks>
1082 		/// This method checks all the definitions in the description and returns a list of errors.
1083 		/// If the returned list is empty, it means that the description is valid.
1084 		/// </remarks>
Verify()1085 		public StringCollection Verify ()
1086 		{
1087 			return Verify (new AddinFileSystemExtension ());
1088 		}
1089 
Verify(AddinFileSystemExtension fs)1090 		internal StringCollection Verify (AddinFileSystemExtension fs)
1091 		{
1092 			StringCollection errors = new StringCollection ();
1093 
1094 			if (IsRoot) {
1095 				if (OptionalModules.Count > 0)
1096 					errors.Add ("Root add-in hosts can't have optional modules.");
1097 			}
1098 
1099 			if (AddinId.Length == 0 || Version.Length == 0) {
1100 				if (ExtensionPoints.Count > 0)
1101 					errors.Add ("Add-ins which define new extension points must have an Id and Version.");
1102 			}
1103 
1104 			MainModule.Verify ("", errors);
1105 			OptionalModules.Verify ("", errors);
1106 			ExtensionNodeSets.Verify ("", errors);
1107 			ExtensionPoints.Verify ("", errors);
1108 			ConditionTypes.Verify ("", errors);
1109 
1110 			foreach (ExtensionNodeSet nset in ExtensionNodeSets) {
1111 				if (nset.Id.Length == 0)
1112 					errors.Add ("Attribute 'id' can't be empty for global node sets.");
1113 			}
1114 
1115 			string bp = null;
1116 			if (BasePath.Length > 0)
1117 				bp = BasePath;
1118 			else if (sourceAddinFile != null && sourceAddinFile.Length > 0)
1119 				bp = Path.GetDirectoryName (AddinFile);
1120 			else if (configFile != null && configFile.Length > 0)
1121 				bp = Path.GetDirectoryName (configFile);
1122 
1123 			if (bp != null) {
1124 				foreach (string file in AllFiles) {
1125 					string asmFile = Path.Combine (bp, file);
1126 					if (!fs.FileExists (asmFile))
1127 						errors.Add ("The file '" + asmFile + "' referenced in the manifest could not be found.");
1128 				}
1129 			}
1130 
1131 			if (localizer != null && localizer.GetAttribute ("type").Length == 0) {
1132 				errors.Add ("The attribute 'type' in the Location element is required.");
1133 			}
1134 
1135 			// Ensure that there are no duplicated properties
1136 
1137 			if (properties != null) {
1138 				HashSet<string> props = new HashSet<string> ();
1139 				foreach (var prop in properties) {
1140 					if (!props.Add (prop.Name + " " + prop.Locale))
1141 						errors.Add (string.Format ("Property {0} specified more than once", prop.Name + (prop.Locale != null ? " (" + prop.Locale + ")" : "")));
1142 				}
1143 			}
1144 
1145 			return errors;
1146 		}
1147 
SetExtensionsAddinId(string addinId)1148 		internal void SetExtensionsAddinId (string addinId)
1149 		{
1150 			foreach (ExtensionPoint ep in ExtensionPoints)
1151 				ep.SetExtensionsAddinId (addinId);
1152 
1153 			foreach (ExtensionNodeSet ns in ExtensionNodeSets)
1154 				ns.SetExtensionsAddinId (addinId);
1155 		}
1156 
UnmergeExternalData(Hashtable addins)1157 		internal void UnmergeExternalData (Hashtable addins)
1158 		{
1159 			// Removes extension types and extension sets coming from other add-ins.
1160 			foreach (ExtensionPoint ep in ExtensionPoints)
1161 				ep.UnmergeExternalData (AddinId, addins);
1162 
1163 			foreach (ExtensionNodeSet ns in ExtensionNodeSets)
1164 				ns.UnmergeExternalData (AddinId, addins);
1165 		}
1166 
MergeExternalData(AddinDescription other)1167 		internal void MergeExternalData (AddinDescription other)
1168 		{
1169 			// Removes extension types and extension sets coming from other add-ins.
1170 			foreach (ExtensionPoint ep in other.ExtensionPoints) {
1171 				ExtensionPoint tep = ExtensionPoints [ep.Path];
1172 				if (tep != null)
1173 					tep.MergeWith (AddinId, ep);
1174 			}
1175 
1176 			foreach (ExtensionNodeSet ns in other.ExtensionNodeSets) {
1177 				ExtensionNodeSet tns = ExtensionNodeSets [ns.Id];
1178 				if (tns != null)
1179 					tns.MergeWith (AddinId, ns);
1180 			}
1181 		}
1182 
1183 		internal bool IsExtensionModel {
1184 			get { return RootElement.LocalName == "ExtensionModel"; }
1185 		}
1186 
Merge(AddinDescription desc1, AddinDescription desc2)1187 		internal static AddinDescription Merge (AddinDescription desc1, AddinDescription desc2)
1188 		{
1189 			if (!desc2.IsExtensionModel) {
1190 				AddinDescription tmp = desc1;
1191 				desc1 = desc2; desc2 = tmp;
1192 			}
1193 			((AddinPropertyCollectionImpl)desc1.Properties).AddRange (desc2.Properties);
1194 			desc1.ExtensionPoints.AddRange (desc2.ExtensionPoints);
1195 			desc1.ExtensionNodeSets.AddRange (desc2.ExtensionNodeSets);
1196 			desc1.ConditionTypes.AddRange (desc2.ConditionTypes);
1197 			desc1.OptionalModules.AddRange (desc2.OptionalModules);
1198 			foreach (string s in desc2.MainModule.Assemblies)
1199 				desc1.MainModule.Assemblies.Add (s);
1200 			foreach (string s in desc2.MainModule.DataFiles)
1201 				desc1.MainModule.DataFiles.Add (s);
1202 			desc1.MainModule.MergeWith (desc2.MainModule);
1203 			return desc1;
1204 		}
1205 
IBinaryXmlElement.Write(BinaryXmlWriter writer)1206 		void IBinaryXmlElement.Write (BinaryXmlWriter writer)
1207 		{
1208 			TransferCoreProperties (true);
1209 			writer.WriteValue ("id", ParseString (id));
1210 			writer.WriteValue ("ns", ParseString (ns));
1211 			writer.WriteValue ("isroot", isroot);
1212 			writer.WriteValue ("name", ParseString (name));
1213 			writer.WriteValue ("version", ParseString (version));
1214 			writer.WriteValue ("compatVersion", ParseString (compatVersion));
1215 			writer.WriteValue ("hasUserId", hasUserId);
1216 			writer.WriteValue ("author", ParseString (author));
1217 			writer.WriteValue ("url", ParseString (url));
1218 			writer.WriteValue ("copyright", ParseString (copyright));
1219 			writer.WriteValue ("description", ParseString (description));
1220 			writer.WriteValue ("category", ParseString (category));
1221 			writer.WriteValue ("basePath", basePath);
1222 			writer.WriteValue ("sourceAddinFile", sourceAddinFile);
1223 			writer.WriteValue ("defaultEnabled", defaultEnabled);
1224 			writer.WriteValue ("domain", domain);
1225 			writer.WriteValue ("MainModule", MainModule);
1226 			writer.WriteValue ("OptionalModules", OptionalModules);
1227 			writer.WriteValue ("NodeSets", ExtensionNodeSets);
1228 			writer.WriteValue ("ExtensionPoints", ExtensionPoints);
1229 			writer.WriteValue ("ConditionTypes", ConditionTypes);
1230 			writer.WriteValue ("FilesInfo", fileInfo);
1231 			writer.WriteValue ("Localizer", localizer);
1232 			writer.WriteValue ("flags", (int)flags);
1233 			writer.WriteValue ("Properties", properties);
1234 		}
1235 
IBinaryXmlElement.Read(BinaryXmlReader reader)1236 		void IBinaryXmlElement.Read (BinaryXmlReader reader)
1237 		{
1238 			id = reader.ReadStringValue ("id");
1239 			ns = reader.ReadStringValue ("ns");
1240 			isroot = reader.ReadBooleanValue ("isroot");
1241 			name = reader.ReadStringValue ("name");
1242 			version = reader.ReadStringValue ("version");
1243 			compatVersion = reader.ReadStringValue ("compatVersion");
1244 			hasUserId = reader.ReadBooleanValue ("hasUserId");
1245 			author = reader.ReadStringValue ("author");
1246 			url = reader.ReadStringValue ("url");
1247 			copyright = reader.ReadStringValue ("copyright");
1248 			description = reader.ReadStringValue ("description");
1249 			category = reader.ReadStringValue ("category");
1250 			basePath = reader.ReadStringValue ("basePath");
1251 			sourceAddinFile = reader.ReadStringValue ("sourceAddinFile");
1252 			defaultEnabled = reader.ReadBooleanValue ("defaultEnabled");
1253 			domain = reader.ReadStringValue ("domain");
1254 			mainModule = (ModuleDescription) reader.ReadValue ("MainModule");
1255 			optionalModules = (ModuleCollection) reader.ReadValue ("OptionalModules", new ModuleCollection (this));
1256 			nodeSets = (ExtensionNodeSetCollection) reader.ReadValue ("NodeSets", new ExtensionNodeSetCollection (this));
1257 			extensionPoints = (ExtensionPointCollection) reader.ReadValue ("ExtensionPoints", new ExtensionPointCollection (this));
1258 			conditionTypes = (ConditionTypeDescriptionCollection) reader.ReadValue ("ConditionTypes", new ConditionTypeDescriptionCollection (this));
1259 			fileInfo = (object[]) reader.ReadValue ("FilesInfo", null);
1260 			localizer = (ExtensionNodeDescription) reader.ReadValue ("Localizer");
1261 			flags = (AddinFlags) reader.ReadInt32Value ("flags");
1262 			properties = (AddinPropertyCollectionImpl) reader.ReadValue ("Properties", new AddinPropertyCollectionImpl (this));
1263 
1264 			if (mainModule != null)
1265 				mainModule.SetParent (this);
1266 		}
1267 	}
1268 
1269 	class AddinFileInfo: IBinaryXmlElement
1270 	{
1271 		string fileName;
1272 		DateTime timestamp;
1273 
1274 		public string FileName {
1275 			get {
1276 				return fileName;
1277 			}
1278 			set {
1279 				fileName = value;
1280 			}
1281 		}
1282 
1283 		public System.DateTime Timestamp {
1284 			get {
1285 				return timestamp;
1286 			}
1287 			set {
1288 				timestamp = value;
1289 			}
1290 		}
1291 
Read(BinaryXmlReader reader)1292 		public void Read (BinaryXmlReader reader)
1293 		{
1294 			fileName = reader.ReadStringValue ("fileName");
1295 			timestamp = reader.ReadDateTimeValue ("timestamp");
1296 		}
1297 
Write(BinaryXmlWriter writer)1298 		public void Write (BinaryXmlWriter writer)
1299 		{
1300 			writer.WriteValue ("fileName", fileName);
1301 			writer.WriteValue ("timestamp", timestamp);
1302 		}
1303 	}
1304 }
1305