From c61563ddcea10c005d1a7f3fbb0408ca41eacb72 Mon Sep 17 00:00:00 2001
From: Jared Reisinger <jaredreisinger@hotmail.com>
Date: Wed, 11 Dec 2013 18:22:15 -0800
Subject: [PATCH] Fixes #21: Added 'defaultImageQuery' attribute support to the
 'watermarks' configuration.  All image watermarks will have values from
 'defaultImageQuery' applied to their 'imageQuery' except where 'imageQuery'
 has an explicit key/value.  If the 'defaultImageQuery' attribute is not
 specified, it defaults to "scache=true".

Also added general-purpose "merge defaults" to NameValueCollectionExtensions and merging-constructors to ResizeSettings and added a unit test to verify the merging behavior.
---
 Core/ExtensionMethods/NameValueCollection.cs | 21 +++++++++++++
 Core/ResizeSettings.cs                       | 16 ++++++++++
 Plugins/Watermark/ImageLayer.cs              | 15 ++++++---
 Plugins/Watermark/Watermark.cs               | 32 +++++++++++++++-----
 Tests/Core.Tests/ResizeSettingsTest.cs       | 18 +++++++++++
 5 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/Core/ExtensionMethods/NameValueCollection.cs b/Core/ExtensionMethods/NameValueCollection.cs
index 2c328031c..0733f8db9 100644
--- a/Core/ExtensionMethods/NameValueCollection.cs
+++ b/Core/ExtensionMethods/NameValueCollection.cs
@@ -243,5 +243,26 @@ public static NameValueCollection Keep(NameValueCollection q, params string[] ke
 
             return c;
         }
+
+        /// <summary>
+        /// Creates and returns a new NameValueCollection instance that contains all of the
+        /// keys/values from 'q', and any keys/values from 'defaults' that 'q' does not already
+        /// contain.
+        /// </summary>
+        /// <param name="q">The settings specific to a particular query</param>
+        /// <param name="defaults">Default settings to use when not overridden by 'q'.</param>
+        /// <returns></returns>
+        public static NameValueCollection MergeDefaults(NameValueCollection q, NameValueCollection defaults)
+        {
+            // Start with the defaults, and then blindly copy the keys/values
+            // from 'q'.  Any keys in both 'defaults' and 'q' will end up having
+            // the value from 'q', since that one is set last.
+            NameValueCollection c = new NameValueCollection(defaults);
+            foreach (string s in q.AllKeys) {
+                c[s] = q[s];
+            }
+
+            return c;
+        }
     }
 }
diff --git a/Core/ResizeSettings.cs b/Core/ResizeSettings.cs
index 514e3b0d5..3bd1fae0b 100644
--- a/Core/ResizeSettings.cs
+++ b/Core/ResizeSettings.cs
@@ -34,6 +34,22 @@ public ResizeSettings(NameValueCollection col) : base(col) { }
         /// </summary>
         /// <param name="queryString"></param>
         public ResizeSettings(string queryString) : base(PathUtils.ParseQueryStringFriendlyAllowSemicolons(queryString)) { }
+        /// <summary>
+        /// Merges the specified collection with a set of defaults into a new
+        /// ResizeSettings instance.
+        /// </summary>
+        /// <param name="col"></param>
+        /// <param name="defaultSettings"></param>
+        public ResizeSettings(NameValueCollection col, NameValueCollection defaultSettings)
+            : base(NameValueCollectionExtensions.MergeDefaults(col, defaultSettings)) { }
+        /// <summary>
+        /// Parses the specified querystring into name/value pairs and merges
+        /// it with defaultSettings in a new ResizeSettings instance.
+        /// </summary>
+        /// <param name="queryString"></param>
+        /// <param name="defaultSettings"></param>
+        public ResizeSettings(string queryString, NameValueCollection defaultSettings)
+            : this(PathUtils.ParseQueryStringFriendlyAllowSemicolons(queryString), defaultSettings) { }
 
         /// <summary>
         /// Creates a new resize settings object with the specified resizing settings
diff --git a/Plugins/Watermark/ImageLayer.cs b/Plugins/Watermark/ImageLayer.cs
index db2270e50..2f589d0d5 100644
--- a/Plugins/Watermark/ImageLayer.cs
+++ b/Plugins/Watermark/ImageLayer.cs
@@ -12,14 +12,19 @@
 
 namespace ImageResizer.Plugins.Watermark {
     public class ImageLayer:Layer {
-        public ImageLayer(NameValueCollection attrs, Config c)
+        public ImageLayer(NameValueCollection attrs, ResizeSettings defaultImageQuery, Config c)
             : base(attrs) {
-                Path = attrs["path"];
-                this.c = c;
-                if (!string.IsNullOrEmpty(attrs["imageQuery"])) ImageQuery = new ResizeSettings(attrs["imageQuery"]);
+            Path = attrs["path"];
+            this.c = c;
+            if (!string.IsNullOrEmpty(attrs["imageQuery"])) {
+                ImageQuery = new ResizeSettings(attrs["imageQuery"], defaultImageQuery);
+            } else {
+                ImageQuery = new ResizeSettings(defaultImageQuery);
+            }
         }
 
-        public ImageLayer(Config c) {
+        public ImageLayer(Config c)
+        {
             this.c = c;
         }
         protected string _path = null;
diff --git a/Plugins/Watermark/Watermark.cs b/Plugins/Watermark/Watermark.cs
index df3f08a30..0d60bf153 100644
--- a/Plugins/Watermark/Watermark.cs
+++ b/Plugins/Watermark/Watermark.cs
@@ -23,11 +23,24 @@ public class WatermarkPlugin : LegacyWatermarkFeatures, IPlugin, IQuerystringPlu
         public WatermarkPlugin() {
         }
 
+
+        private ResizeSettings _defaultImageQuery = new ResizeSettings("scache=true");
+        /// <summary>
+        /// Default querystring parameters for all image watermarks.
+        /// If not specified in the watermark configuration, defaults to
+        /// "scache=true".
+        /// </summary>
+        public ResizeSettings DefaultImageQuery
+        {
+            get { return _defaultImageQuery; }
+            set { _defaultImageQuery = value; }
+        }
+
         ImageLayer _otherImages = new ImageLayer(null);
         /// <summary>
         /// When a &amp;watermark command does not specify a named preset, it is assumed to be a file name. 
         /// Set OtherImages.Path to the search folder. All watermark images (except for presets) must be in the root of the search folder. 
-        /// The remainder of the settings affect how each watermrak will be positioned and displayed.
+        /// The remainder of the settings affect how each watermark will be positioned and displayed.
         /// </summary>
         public ImageLayer OtherImages {
             get { return _otherImages; }
@@ -45,7 +58,7 @@ public IPlugin Install(Configuration.Config c) {
             c.Plugins.add_plugin(this);
             this.c = c;
             this.OtherImages.ConfigInstance = c;
-            _namedWatermarks = ParseWatermarks(c.getConfigXml().queryFirst("watermarks"), ref _otherImages);
+            _namedWatermarks = ParseWatermarks(c.getConfigXml().queryFirst("watermarks"), ref _defaultImageQuery, ref _otherImages);
             c.Pipeline.PostRewrite += Pipeline_PostRewrite;
             return this;
         }
@@ -60,7 +73,13 @@ public IEnumerable<string> GetSupportedQuerystringKeys() {
             return new string[] { "watermark" };
         }
 
-        protected Dictionary<string, IEnumerable<Layer>> ParseWatermarks(Node n, ref ImageLayer otherImageDefaults) {
+        protected Dictionary<string, IEnumerable<Layer>> ParseWatermarks(Node n, ref ResizeSettings defaultImageQuery, ref ImageLayer otherImageDefaults) {
+            // Grab the defaultImageQuery value (if it exists) from the watermarks
+            // node, so that we can apply them to any subsequent image watermarks.
+            if (n != null && !string.IsNullOrEmpty(n.Attrs["defaultImageQuery"])) {
+                defaultImageQuery = new ResizeSettings(n.Attrs["defaultImageQuery"]);
+            }
+
             Dictionary<string, IEnumerable<Layer>> dict = new Dictionary<string, IEnumerable<Layer>>(StringComparer.OrdinalIgnoreCase);
             if (n == null || n.Children == null) return dict;
             foreach (Node c in n.Children) {
@@ -77,16 +96,15 @@ protected Dictionary<string, IEnumerable<Layer>> ParseWatermarks(Node n, ref Ima
                 }
 
                 
-
-                if (c.Name.Equals("otherimages", StringComparison.OrdinalIgnoreCase)) otherImageDefaults = new ImageLayer(c.Attrs, this.c);
-                if (c.Name.Equals("image", StringComparison.OrdinalIgnoreCase)) dict.Add(name, new Layer[]{new ImageLayer(c.Attrs, this.c)});
+                if (c.Name.Equals("otherimages", StringComparison.OrdinalIgnoreCase)) otherImageDefaults = new ImageLayer(c.Attrs, defaultImageQuery, this.c);
+                if (c.Name.Equals("image", StringComparison.OrdinalIgnoreCase)) dict.Add(name, new Layer[] { new ImageLayer(c.Attrs, defaultImageQuery, this.c) });
                 if (c.Name.Equals("text", StringComparison.OrdinalIgnoreCase)) dict.Add(name, new Layer[] {new TextLayer(c.Attrs) });
                 if (c.Name.Equals("group", StringComparison.OrdinalIgnoreCase)) {
                     
                     List<Layer> layers = new List<Layer>();
                     if (c.Children != null) {
                         foreach (Node layer in c.Children) {
-                            if (layer.Name.Equals("image", StringComparison.OrdinalIgnoreCase)) layers.Add(new ImageLayer(layer.Attrs, this.c));
+                            if (layer.Name.Equals("image", StringComparison.OrdinalIgnoreCase)) layers.Add(new ImageLayer(layer.Attrs, defaultImageQuery, this.c));
                             if (layer.Name.Equals("text", StringComparison.OrdinalIgnoreCase)) layers.Add(new TextLayer(layer.Attrs));
                         }
                     }
diff --git a/Tests/Core.Tests/ResizeSettingsTest.cs b/Tests/Core.Tests/ResizeSettingsTest.cs
index 26bcd366e..9071363d1 100644
--- a/Tests/Core.Tests/ResizeSettingsTest.cs
+++ b/Tests/Core.Tests/ResizeSettingsTest.cs
@@ -55,5 +55,23 @@ public void TestRoundTripColor(string from, string expected)
             Color? parsed = ParseUtils.ParseColor(from).Value;
             Assert.AreEqual(expected, ParseUtils.SerializeColor(parsed.Value), StringComparison.InvariantCultureIgnoreCase);
         }
+
+        [Test]
+        [Row("a=1", "", "?a=1")]
+        [Row("", "b=2", "?b=2")]
+        [Row("a=1", "b=2", "?a=1&b=2")]
+        [Row("b=1", "b=2", "?b=1")]
+        public void TestMergingConstructor(string q, string defaults, string expected)
+        {
+            ResizeSettings defaultSettings = new ResizeSettings(defaults);
+            ResizeSettings mergedSettings = new ResizeSettings(q, defaultSettings);
+            ResizeSettings expectedSettings = new ResizeSettings(expected);
+
+            Assert.AreEqual(expectedSettings.Count, mergedSettings.Count);
+            foreach (string key in expectedSettings.AllKeys)
+            {
+                Assert.AreEqual(expectedSettings[key], mergedSettings[key]);
+            }
+        }
     }
 }