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