View Javadoc

1   package com.atlassian.sal.core.pluginsettings;
2   
3   import java.io.*;
4   import java.util.*;
5   import java.util.Map.Entry;
6   
7   import com.atlassian.sal.api.pluginsettings.PluginSettings;
8   import com.google.common.collect.Lists;
9   import com.google.common.base.Function;
10  import org.apache.commons.lang.Validate;
11  import org.apache.log4j.Logger;
12  
13  /**
14   * PluginSettings implementation for datastores that only support Strings.  Handles converting Strings into Lists and
15   * Properties objects using a '#TYPE_IDENTIFIER' header on the string.
16   */
17  public abstract class AbstractStringPluginSettings implements PluginSettings
18  {
19      private static final Logger log = Logger.getLogger(AbstractStringPluginSettings.class);
20  
21      private static final String PROPERTIES_ENCODING = "ISO8859_1";
22      private static final String PROPERTIES_IDENTIFIER = "java.util.Properties";
23      private static final String LIST_IDENTIFIER = "#java.util.List";
24      private static final String MAP_IDENTIFIER = "#java.util.Map";
25  
26      /**
27       * Only some applications support >100 character keys, so plugins should
28       * not depend on it. In developer mode this restriction is enforced to
29       * highlight plugin bugs. As some applications support more, and some
30       * existing plugins rely on this behaviour, the limit is relaxed in
31       * production.
32       */
33      private final boolean isDeveloperMode = Boolean.getBoolean("atlassian.dev.mode");
34      
35      /**
36       * Puts a setting value.
37       *
38       * @param key Setting key.  Cannot be null
39       * @param value Setting value.  Must be one of {@link String}, {@link List<String>}, {@link Properties}, {@link
40       * Map<String, String>}, or null. null will remove the item from the settings.
41       * @return The setting value that was over ridden. Null if none existed.
42       * @throws IllegalArgumentException if value is not {@link String}, {@link List<String>}, {@link Properties}, {@link
43       * Map<String, String>}, or null.
44       */
45      public Object put(String key, Object value)
46      {
47          Validate.notNull(key, "The plugin settings key cannot be null");
48          Validate.isTrue(key.length() <= 255, "The plugin settings key cannot be more than 255 characters");
49          if (isDeveloperMode)
50          {
51              Validate.isTrue(key.length() <= 100, "The plugin settings key cannot be more than 100 characters in developer mode");
52          }
53          if (value == null)
54          {
55              return remove(key);
56          }
57  
58          final Object oldValue = get(key);
59          if (value instanceof Properties)
60          {
61              final ByteArrayOutputStream bout = new ByteArrayOutputStream();
62              try
63              {
64  
65                  final Properties properties = (Properties) value;
66                  properties.store(bout, PROPERTIES_IDENTIFIER);
67                  putActual(key, new String(bout.toByteArray(), PROPERTIES_ENCODING));
68              }
69              catch (final IOException e)
70              {
71                  throw new IllegalArgumentException("Unable to serialize properties", e);
72              }
73          }
74          else if (value instanceof String)
75          {
76              putActual(key, (String) value);
77          }
78          else if (value instanceof List)
79          {
80              final StringBuilder sb = new StringBuilder();
81              sb.append(LIST_IDENTIFIER).append(EscapeUtils.NEW_LINE);
82              for (final Iterator i = ((List) value).iterator(); i.hasNext();)
83              {
84                  sb.append(EscapeUtils.escape(i.next().toString()));
85                  if (i.hasNext())
86                      sb.append(EscapeUtils.NEW_LINE);
87              }
88              putActual(key, sb.toString());
89          }
90          else if (value instanceof Map)
91          {
92              final StringBuilder sb = new StringBuilder();
93              sb.append(MAP_IDENTIFIER).append(EscapeUtils.NEW_LINE);
94              for (final Iterator<Entry> i = ((Map) value).entrySet().iterator(); i.hasNext();)
95              {
96                  final Entry entry = i.next();
97                  sb.append(EscapeUtils.escape(entry.getKey().toString()));
98                  sb.append(EscapeUtils.VERTICAL_TAB);
99                  sb.append(EscapeUtils.escape(entry.getValue().toString()));
100                 if (i.hasNext())
101                     sb.append(EscapeUtils.NEW_LINE);
102             }
103             putActual(key, sb.toString());
104         }
105         else
106         {
107             throw new IllegalArgumentException("Property type: " + value.getClass() + " not supported");
108         }
109         return oldValue;
110     }
111 
112     /**
113      * Gets a setting value. The setting returned should be specific to this context settings object and not cascade the
114      * value to a global context.
115      *
116      * @param key The setting key.  Cannot be null
117      * @return The setting value. May be null
118      */
119     public Object get(String key)
120     {
121         Validate.notNull(key, "The plugin settings key cannot be null");
122         if (isDeveloperMode && key.length() > 100)
123         {
124             log.warn("PluginSettings.get with excessive key length: " + key);
125         }
126         final String val = getActual(key);
127         if (val != null && val.startsWith("#" + PROPERTIES_IDENTIFIER))
128         {
129             final Properties p = new Properties();
130             try
131             {
132                 p.load(new ByteArrayInputStream(val.getBytes(PROPERTIES_ENCODING)));
133             }
134             catch (final IOException e)
135             {
136                 throw new IllegalArgumentException("Unable to deserialize properties", e);
137             }
138             return p;
139         }
140         else if (val != null && val.startsWith(LIST_IDENTIFIER))
141         {
142             final String[] lines = val.split(EscapeUtils.NEW_LINE + "");
143 
144             // remove the first item since it's the list identifier.
145             final List<String> rawItems = Arrays.asList(lines).subList(1, lines.length);
146 
147             // unescape each of the items.
148             final List<String> items = Lists.transform(rawItems, new Function<String, String>()
149                                                                  {
150                                                                         public String apply(String from)
151                                                                         {
152                                                                             return EscapeUtils.unescape(from);
153                                                                         }
154                                                                  });
155             return new ArrayList<String>(items);
156         }
157         else if (val != null && val.startsWith(MAP_IDENTIFIER))
158         {
159             String nval = val.substring(MAP_IDENTIFIER.length() + 1);
160             final HashMap<String, String> map = new HashMap<String, String>();
161             final String[] items = nval.split(EscapeUtils.NEW_LINE + "");
162             for (String item : items)
163             {
164                 if (item.length() > 0)
165                 {
166                     String[] pair = item.split(EscapeUtils.VERTICAL_TAB + "");
167                     if (pair.length != 2)
168                     {
169                         log.error("Could not parse map element: << " + item + " >> \n" +
170                             "Full list: \n" + nval);
171                     }
172                     else
173                     {
174                         map.put(EscapeUtils.unescape(pair[0]),
175                                 EscapeUtils.unescape(pair[1]));
176                     }    
177                 }
178             }
179 
180             return map;
181         }
182         else
183         {
184             return val;
185         }
186     }
187 
188     /**
189      * Removes a setting value
190      *
191      * @param key The setting key
192      * @return The setting value that was removed. Null if nothing was removed.
193      */
194     public Object remove(String key)
195     {
196         Validate.notNull(key, "The plugin settings key cannot be null");
197         if (isDeveloperMode && key.length() > 100)
198         {
199             log.warn("PluginSettings.get with excessive key length: " + key);
200         }
201         Object oldValue = get(key);
202         if (oldValue != null)
203         {
204             removeActual(key);
205         }
206         return oldValue;
207     }
208 
209     /**
210      * Put the actual value.
211      *
212      * @param key The key to put it at.
213      * @param val The value
214      */
215     protected abstract void putActual(String key, String val);
216 
217     /**
218      * Get the actual value
219      *
220      * @param key The key to get
221      * @return The value
222      */
223     protected abstract String getActual(String key);
224 
225     /**
226      * Do the actual remove.  This will only be called if the value already exists.
227      *
228      * @param key The key to remove
229      */
230     protected abstract void removeActual(String key);
231 
232 }