Author: nbubna
Date: Thu Feb 12 23:37:52 2009
New Revision: 743927

URL: http://svn.apache.org/viewvc?rev=743927&view=rev
Log:
VELTOOLS-113 add br(), stripTags(), word-sensitive truncate(), property-based 
list(), and plural() (thanks to Sergiy Kovalchuk)

Modified:
    velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties
    
velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/DisplayTool.java
    
velocity/tools/trunk/src/test/java/org/apache/velocity/tools/DisplayToolTests.java

Modified: 
velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties
URL: 
http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties?rev=743927&r1=743926&r2=743927&view=diff
==============================================================================
--- velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties 
(original)
+++ velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties Thu 
Feb 12 23:37:52 2009
@@ -253,6 +253,7 @@
 display.truncate_ObjectintString = Limits the string output of the second 
argument to the specified number of characters. If the string gets curtailed, 
the specified suffix is used as the ending of the truncated string.
 display.alt = Returns the configured default value ("null" by default) if the 
specified value is null.
 display.alt_ObjectObject = Returns the second argument if first argument 
specified is null.
+display.br_Object = Inserts HTML line break tag (<br />) in front of all 
newline characters of the string value of the specified object and returns the 
resulting string.
 display.cell = Truncates or pads the string value of the specified object as 
necessary to ensure that the returned string''s length equals the default cell 
size.
 display.cell_Objectint = Truncates or pads the string value of the specified 
object as necessary to ensure that the returned string''s length equals the 
specified cell size.
 display.cell_ObjectString = Truncates or pads the string value of the 
specified object as necessary to ensure that the returned string''s length 
equals the default cell size. If truncation is necessary, the specified suffix 
will replace the end of the string value to indicate that.
@@ -260,11 +261,17 @@
 display.message = Uses <code>java.text.MessageFormat</code> to format the 
specified String with the specified arguments. If there are no arguments, then 
the String is returned directly.
 display.message_StringObjectVarArgs.param1 = ''foo {1} {0}''
 display.message_StringObjectVarArgs.param2 = [''bar'', 2]
+display.plural_intString = Builds plural form of a passed word if ''value'' is 
plural, otherwise returns ''singular''. Plural form is built using some basic 
English language rules for nouns which does not guarantee correct syntax of a 
result in all cases.
+display.plural_intStringString = Returns ''plural'' parameter if passed 
'value' is plural, otherwise ''singular'' is returned.
+display.printf_StringObjectVarArgs = Uses 
String.format(Locale,String,Object...) to format the specified String with the 
specified arguments.  Please note that the format required here is quite 
different from that of message(String,Object...).
 display.space = Returns a string of spaces of the specified length.
 display.space_int.param1 = 4
+display.stripTags_Object = Removes HTML tags from the string value of the 
specified object and returns the resulting string.
+display.stripTags_ObjectStringVarArgs = Removes all not allowed HTML tags from 
the string value of the specified object and returns the resulting string.
 display.capitalize = Changes the first character of the string value of the 
specified object to upper case and returns the resulting string.
 display.uncapitalize = Changes the first character of the string value of the 
specified object to lower case and returns the resulting string.
 display.measure = Returns the measurements of the string value of the 
specified object.
+display.getAllowedTags = Returns the configured tags allowed to remain by 
stripTags(Object).
 display.getCellLength = Returns the configured default cell size.
 display.getCellSuffix = Returns the configured default suffix used when cell 
contents need truncating.
 display.getDefaultAlternate = Returns the configured default alternate used by 
alt(Object).
@@ -272,6 +279,8 @@
 display.getListFinalDelimiter = Returns the configured default delimiter used 
between the last two items by the list formatting methods.
 display.getTruncateLength = Returns the configured default length used by 
truncate(Object).
 display.getTruncateSuffix = Returns the configured default suffix used by the 
truncate methods.
+display.getTruncateAtWord = Returns the configured default setting for whether 
to truncate at a precise character or the last fitting word.
+display.Object = ''string''
 
 # field.vm resources
 field.intro = This tool allows easy access to public static fields in classes, 
 such as string constants.

Modified: 
velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/DisplayTool.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/DisplayTool.java?rev=743927&r1=743926&r2=743927&view=diff
==============================================================================
--- 
velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/DisplayTool.java
 (original)
+++ 
velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/DisplayTool.java
 Thu Feb 12 23:37:52 2009
@@ -25,6 +25,9 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.regex.Pattern;
+
+import org.apache.commons.beanutils.PropertyUtils;
 import org.apache.velocity.tools.config.DefaultKey;
 
 /**
@@ -49,7 +52,7 @@
  * template...
  *   #set( $list = [1..5] )
  *   $display.list($list)
- *   $display.truncate(10, "This is a long string.")
+ *   $display.truncate("This is a long string.", 10)
  *   Not Null: $display.alt("not null", "--")
  *   Null: $display.alt($null, "--")
  *
@@ -74,17 +77,21 @@
     public static final String LIST_FINAL_DELIM_KEY = "listFinalDelim";
     public static final String TRUNCATE_LENGTH_KEY = "truncateLength";
     public static final String TRUNCATE_SUFFIX_KEY = "truncateSuffix";
+    public static final String TRUNCATE_AT_WORD_KEY = "truncateAtWord";
     public static final String CELL_LENGTH_KEY = "cellLength";
     public static final String CELL_SUFFIX_KEY = "cellSuffix";
     public static final String DEFAULT_ALTERNATE_KEY = "defaultAlternate";
+    public static final String ALLOWED_TAGS_KEY = "allowedTags";
 
     private String defaultDelim = ", ";
     private String defaultFinalDelim = " and ";
     private int defaultTruncateLength = 30;
     private String defaultTruncateSuffix = "...";
+    private boolean defaultTruncateAtWord = false;
     private int defaultCellLength = 30;
     private String defaultCellSuffix = "...";
     private String defaultAlternate = "null";
+    private String[] defaultAllowedTags = null;
 
     /**
      * Does the actual configuration. This is protected, so
@@ -118,6 +125,12 @@
             setTruncateSuffix(truncateSuffix);
         }
 
+        Boolean truncateAtWord = values.getBoolean(TRUNCATE_AT_WORD_KEY);
+        if (truncateAtWord != null)
+        {
+            setTruncateAtWord(truncateAtWord);
+        }
+
         Integer cellLength = values.getInteger(CELL_LENGTH_KEY);
         if (cellLength != null)
         {
@@ -135,6 +148,12 @@
         {
             setDefaultAlternate(defaultAlternate);
         }
+
+        String[] allowedTags = values.getStrings(ALLOWED_TAGS_KEY);
+        if (allowedTags != null)
+        {
+            setAllowedTags(allowedTags);
+        }
     }
 
     public String getListDelimiter()
@@ -177,6 +196,16 @@
         this.defaultTruncateSuffix = suffix;
     }
 
+    public boolean getTruncateAtWord()
+    {
+        return this.defaultTruncateAtWord;
+    }
+
+    protected void setTruncateAtWord(boolean atWord)
+    {
+        this.defaultTruncateAtWord = atWord;
+    }
+
     public String getCellSuffix()
     {
         return this.defaultCellSuffix;
@@ -207,6 +236,16 @@
         this.defaultAlternate = dflt;
     }
 
+    public String[] getAllowedTags()
+    {
+        return this.defaultAllowedTags;
+    }
+
+    protected void setAllowedTags(String[] tags)
+    {
+        this.defaultAllowedTags = tags;
+    }
+
 
     /**
      * Formats a collection or array into the form "A, B and C".
@@ -235,7 +274,7 @@
     /**
      * Formats a collection or array into the form
      * "A&lt;delim&gt;B&lt;finaldelim&gt;C".
-     *
+     * 
      * @param list A collection or array.
      * @param delim A String.
      * @param finaldelim A String.
@@ -243,20 +282,36 @@
      */
     public String list(Object list, String delim, String finaldelim)
     {
+        return list(list, delim, finaldelim, null);
+    }
+
+    /**
+     * Formats a specified property of collection or array of objects into the
+     * form "A&lt;delim&gt;B&lt;finaldelim&gt;C".
+     * 
+     * @param list A collection or array.
+     * @param delim A String.
+     * @param finaldelim A String.
+     * @param property An object property to format.
+     * @return A String.
+     */
+    public String list(Object list, String delim, String finaldelim,
+                       String property)
+    {
         if (list == null)
         {
             return null;
         }
         if (list instanceof Collection)
         {
-            return format((Collection)list, delim, finaldelim);
+            return format((Collection) list, delim, finaldelim, property);
         }
         Collection items;
         if (list.getClass().isArray())
         {
             int size = Array.getLength(list);
             items = new ArrayList(size);
-            for (int i=0; i < size; i++)
+            for (int i = 0; i < size; i++)
             {
                 items.add(Array.get(list, i));
             }
@@ -265,20 +320,28 @@
         {
             items = Collections.singletonList(list);
         }
-        return format(items, delim, finaldelim);
+        return format(items, delim, finaldelim, property);
     }
 
     /**
      * Does the actual formatting of the collection.
      */
-    protected String format(Collection list, String delim, String finaldelim)
+    protected String format(Collection list, String delim, String finaldelim,
+                            String property)
     {
         StringBuilder sb = new StringBuilder();
         int size = list.size();
         Iterator iterator = list.iterator();
         for (int i = 0; i < size; i++)
         {
-            sb.append(iterator.next());
+            if (property != null && property.length() > 0)
+            {
+                sb.append(getProperty(iterator.next(), property));
+            }
+            else
+            {
+                sb.append(iterator.next());
+            }
             if (i < size - 2)
             {
                 sb.append(delim);
@@ -430,10 +493,10 @@
     }
 
     /**
-     * Limits the string value of 'truncateMe' to the specified max length
-     * in characters. If the string gets curtailed, the specified suffix
-     * is used as the ending of the truncated string.
-     *
+     * Limits the string value of 'truncateMe' to the specified max length in
+     * characters. If the string gets curtailed, the specified suffix is used 
as
+     * the ending of the truncated string.
+     * 
      * @param truncateMe The value to be truncated.
      * @param maxLength An int with the maximum length.
      * @param suffix A String.
@@ -441,6 +504,23 @@
      */
     public String truncate(Object truncateMe, int maxLength, String suffix)
     {
+        return truncate(truncateMe, maxLength, suffix, defaultTruncateAtWord);
+    }
+
+    /**
+     * Limits the string value of 'truncateMe' to the latest complete word
+     * within the specified maxLength. If the string gets curtailed, the
+     * specified suffix is used as the ending of the truncated string.
+     * 
+     * @param truncateMe The value to be truncated.
+     * @param maxLength An int with the maximum length.
+     * @param suffix A String.
+     * @param defaultTruncateAtWord Truncate at a word boundary if true.
+     * @return A String.
+     */
+    public String truncate(Object truncateMe, int maxLength, String suffix,
+                           boolean defaultTruncateAtWord)
+    {
         if (truncateMe == null || maxLength <= 0)
         {
             return null;
@@ -456,8 +536,19 @@
             // either no need or no room for suffix
             return string.substring(0, maxLength);
         }
-        // truncate early and append suffix
+        if (defaultTruncateAtWord)
+        {
+            // find the latest space within maxLength
+            int lastSpace = string.substring(0, maxLength - suffix.length() + 
1)
+                            .lastIndexOf(" ");
+            if (lastSpace > suffix.length())
+            {
+                return string.substring(0, lastSpace) + suffix;
+            }
+        }
+        // truncate to exact character and append suffix
         return string.substring(0, maxLength - suffix.length()) + suffix;
+
     }
 
     /**
@@ -626,6 +717,154 @@
     }
 
     /**
+     * Inserts HTML line break tag (&lt;br /&gt;) in front of all newline
+     * characters of the string value of the specified object and returns the
+     * resulting string.
+     * @param obj
+     */
+    public String br(Object obj)
+    {
+        if (obj == null) 
+        {
+            return null;
+        }
+        else
+        {
+            return String.valueOf(obj).replaceAll("\n", "<br />\n");
+        }
+    }
+
+    /**
+     * Removes HTML tags from the string value of the specified object and
+     * returns the resulting string.
+     * @param obj
+     */
+    public String stripTags(Object obj)
+    {
+        return stripTags(obj, defaultAllowedTags);
+    }
+
+    /**
+     * Removes all not allowed HTML tags from the string value of the specified
+     * object and returns the resulting string.
+     * @param obj
+     * @param allowedTags An array of allowed tag names (i.e. "h1","br","img")
+     */
+    public String stripTags(Object obj, String... allowedTags)
+    {
+        if (obj == null)
+        {
+            return null;
+        }
+        
+        //build list of tags to be used in regex pattern
+        StringBuilder allowedTagList = new StringBuilder();
+        if (allowedTags != null)
+        {
+            for (String tag : allowedTags)
+            {
+                if (tag !=null && tag.matches("[a-zA-Z0-9]+"))
+                {
+                    if (allowedTagList.length() > 0)
+                    {
+                        allowedTagList.append("|");
+                    }
+                    allowedTagList.append(tag);
+                }
+            }
+        }
+        String tagRule = "<[^>]*?>";
+        if (allowedTagList.length() > 0)
+        {
+            tagRule = "<(?!/?(" + allowedTagList.toString() + 
")[\\s>/])[^>]*?>";
+        }
+        return Pattern.compile(tagRule, Pattern.CASE_INSENSITIVE)
+                .matcher(String.valueOf(obj)).replaceAll("");
+    }
+
+    /**
+     * Builds plural form of a passed word if 'value' is plural, otherwise
+     * returns 'singular'. Plural form is built using some basic English
+     * language rules for nouns which does not guarantee correct syntax of a
+     * result in all cases.
+     * @param value
+     * @param singular Singular form of a word.
+     */
+    public String plural(int value, String singular)
+    {
+        return plural(value, singular, null);
+    }
+
+    /**
+     * Returns 'plural' parameter if passed 'value' is plural, otherwise
+     * 'singular' is returned.
+     * @param value
+     * @param singular Singular form of a word.
+     * @param plural Plural form of a word.
+     */
+    public String plural(int value, String singular, String plural)
+    {
+        if (value == 1 || value == -1)
+        {
+            return singular;
+        }
+        else if (plural != null)
+        {
+            return plural;
+        }
+        else if (singular == null || singular.length() == 0)
+        {
+            return singular;
+        }
+        else
+        {
+            //if the last letter is capital then we will append capital 
letters 
+            boolean isCapital = !singular.substring(singular.length() - 1)
+                                .toLowerCase().equals(singular
+                                .substring(singular.length() - 1));
+            
+            String word = singular.toLowerCase();
+            
+            if (word.endsWith("x") || word.endsWith("sh")
+                    || word.endsWith("ch") || word.endsWith("s"))
+            {
+                return singular.concat(isCapital ? "ES" : "es");
+            }
+            else if (word.length() > 1
+                    && word.endsWith("y")
+                    && !word.substring(word.length() - 2, word.length() - 1)
+                            .matches("[aeiou]"))
+            {
+                return singular.substring(0, singular.length() - 1)
+                        .concat(isCapital ? "IES" : "ies");
+            }
+            else
+            {
+                return singular.concat(isCapital ? "S" : "s");
+            }
+        }
+    }
+
+    /**
+     * Safely retrieves the specified property from the specified object.
+     * Subclasses that wish to perform more advanced, efficient, or just
+     * different property retrieval methods should override this method to do
+     * so.
+     */
+    protected Object getProperty(Object object, String property)
+    {
+        try
+        {
+            return PropertyUtils.getProperty(object, property);
+        }
+        catch (Exception e)
+        {
+            throw new IllegalArgumentException("Could not retrieve '"
+                    + property + "' from " + object + ": " + e);
+        }
+    }
+
+    /**
      * Returns the {...@link Measurements} of the string value of the 
specified object.
      */
     public Measurements measure(Object measureMe)

Modified: 
velocity/tools/trunk/src/test/java/org/apache/velocity/tools/DisplayToolTests.java
URL: 
http://svn.apache.org/viewvc/velocity/tools/trunk/src/test/java/org/apache/velocity/tools/DisplayToolTests.java?rev=743927&r1=743926&r2=743927&view=diff
==============================================================================
--- 
velocity/tools/trunk/src/test/java/org/apache/velocity/tools/DisplayToolTests.java
 (original)
+++ 
velocity/tools/trunk/src/test/java/org/apache/velocity/tools/DisplayToolTests.java
 Thu Feb 12 23:37:52 2009
@@ -22,6 +22,7 @@
 import org.junit.*;
 import static org.junit.Assert.*;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -110,17 +111,24 @@
         conf.put(DisplayTool.LIST_FINAL_DELIM_KEY, " und ");
         conf.put(DisplayTool.TRUNCATE_LENGTH_KEY, "5");
         conf.put(DisplayTool.TRUNCATE_SUFFIX_KEY, ">");
+        conf.put(DisplayTool.TRUNCATE_AT_WORD_KEY, "true");
         conf.put(DisplayTool.CELL_LENGTH_KEY, "4");
         conf.put(DisplayTool.CELL_SUFFIX_KEY, "~");
         conf.put(DisplayTool.DEFAULT_ALTERNATE_KEY, "n/a");
+        conf.put(DisplayTool.ALLOWED_TAGS_KEY, "img,br");
         display.configure(conf);
         assertEquals(";", display.getListDelimiter());
         assertEquals(" und ", display.getListFinalDelimiter());
         assertEquals(5, display.getTruncateLength());
         assertEquals(">", display.getTruncateSuffix());
+        assertEquals(true, display.getTruncateAtWord());
         assertEquals("~", display.getCellSuffix());
         assertEquals(4, display.getCellLength());
         assertEquals("n/a", display.getDefaultAlternate());
+        String[] tags = display.getAllowedTags();
+        assertNotNull(tags);
+        assertEquals("img", tags[0]);
+        assertEquals("br", tags[1]);
 
         // ensure that configure is locked now
         conf.put(DisplayTool.LIST_DELIM_KEY, " & ");
@@ -204,6 +212,31 @@
         assertEquals("123", display.list(nums, "", ""));
         assertEquals("1; 2 und 3", display.list(nums, "; ", " und "));
     }
+    
+    public @Test void methodList_ObjectStringStringString() throws Exception
+    {
+        TestBean bean1 = new TestBean(1, "one");
+        TestBean bean2 = new TestBean(2, "two");
+        TestBean bean3 = new TestBean(3, "three");
+        TestBean[] beanArray = new TestBean[] { bean1, bean2, bean3 };
+        List<TestBean> beanList = new ArrayList<TestBean>();
+        beanList.addAll(Arrays.asList(beanArray));
+        
+        DisplayTool display = new DisplayTool();
+        assertEquals(null, display.list(null, null, null, null));
+        assertEquals("1null2null3", display.list(beanArray, null, null, 
"num"));
+        assertEquals("123", display.list(beanList, "", "", "num"));
+        assertEquals("one, two or three", display.list(beanList, ", ", " or ", 
"str"));
+    }
+
+    public @Test void methodSetAllowedTags_StringArray() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        assertNull(display.getAllowedTags());
+        String[] tags = new String[] { "img" };
+        display.setAllowedTags(tags);
+        assertEquals(tags, display.getAllowedTags());
+    }
 
     public @Test void methodSetCellLength_int() throws Exception
     {
@@ -254,6 +287,14 @@
         assertEquals("foo", display.getTruncateSuffix());
     }
 
+    public @Test void methodSetTruncateAtWord_String() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        assertEquals(false, display.getTruncateAtWord());
+        display.setTruncateAtWord(true);
+        assertEquals(true, display.getTruncateAtWord());
+    }
+
     public @Test void methodSpace_int() throws Exception
     {
         DisplayTool display = new DisplayTool();
@@ -308,6 +349,16 @@
         assertEquals("foob", display.truncate("foobar", 4, null));
         assertEquals("foo>", display.truncate("foobar", 4, ">"));
     }
+    
+    public @Test void methodTruncate_ObjectintStringboolean() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        assertEquals(null, display.truncate(null, 0, null, true));
+        assertEquals(null, display.truncate("foo", 0, null, false));
+        assertEquals("f", display.truncate("foo", 1, null, true));
+        assertEquals("long stri>", display.truncate("long string", 10, ">", 
false));
+        assertEquals("long>", display.truncate("long string", 10, ">", true));
+    }
 
     public @Test void methodUncapitalize_Object() throws Exception
     {
@@ -318,7 +369,92 @@
         assertEquals("test", display.uncapitalize("Test"));
         assertEquals("tEST", display.uncapitalize("TEST"));
     }
+    
+    public @Test void methodBr_Object() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        assertEquals(null, display.br(null));
+        assertEquals("", display.br(""));
+        assertEquals("<br />\n", display.br("\n"));
+        assertEquals("line1 <br />\n LINE2", display.br("line1 \n LINE2"));
+    }
+    
+    public @Test void methodStripTags_Object() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        String html = "<p>paragraph <a href=\"url\" target='t'>link</a></p> "
+                      + "<h1>header1</h1> <h2>header2</h2> "
+                      + "<br><br/><br  /><b>bold</b>";
+        assertEquals(null, display.stripTags(null));
+        assertEquals("", display.stripTags(""));
+        assertEquals("paragraph link header1 header2 bold", 
display.stripTags(html));
+    }
+    
+    public @Test void methodStripTags_ObjectStringVarArgs() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        String html = "<p>paragraph <a href=\"url\" target='t'>link</a></p> "
+                      + "<h1>header1</h1> <h2>header2</h2> "
+                      + "<br><br/><br  /><b>bold</b>";
+        assertEquals(null, display.stripTags(null, (String[])null));
+        assertEquals("", display.stripTags("","",""));
+        assertEquals("paragraph link <h1>header1</h1> <h2>header2</h2> bold", 
+                display.stripTags(html, "h1", "h2"));
+        assertEquals("paragraph <a href=\"url\" target='t'>link</a> header1 
header2 bold", 
+                display.stripTags(html, "a"));
+        assertEquals("paragraph link header1 header2 <br><br/><br  
/><b>bold</b>", 
+                display.stripTags(html, "b", "", null, "br"));
+    }
+    
+    public @Test void methodPlural_intString() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        assertEquals(null, display.plural(1,null));
+        assertEquals("", display.plural(2,""));
+        assertEquals("items", display.plural(0,"item"));
+        assertEquals("item", display.plural(-1,"item"));
+        assertEquals("555s", display.plural(2,"555"));
+        assertEquals("TOYS", display.plural(2,"TOY"));
+        assertEquals("ladies", display.plural(2,"lady"));
+        assertEquals("foxes", display.plural(2,"fox"));
+        assertEquals("churches", display.plural(2,"church"));
+    }
+    
+    public @Test void methodPlural_intStringString() throws Exception
+    {
+        DisplayTool display = new DisplayTool();
+        assertEquals(null, display.plural(1,null,null));
+        assertEquals("", display.plural(2,"empty",""));
+        assertEquals("men", display.plural(0,"man","men"));
+        assertEquals("mouse", display.plural(-1,"mouse", "mice"));
+    }
 
-
-}
+    public class TestBean {
+        private int num;
+        private String str;
+        
+        public TestBean(int num, String str)
+        {
+            this.num = num;
+            this.str = str;
+        }
         
+        public int getNum()
+        {
+            return num;
+        }
+        public void setNum(int num)
+        {
+            this.num = num;
+        }
+        public String getStr()
+        {
+            return str;
+        }
+        public void setStr(String str)
+        {
+            this.str = str;
+        }
+    }
+
+}
\ No newline at end of file


Reply via email to