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