View Javadoc

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