View Javadoc

1   package com.atlassian.sal.core.features;
2   
3   import com.atlassian.sal.api.features.SiteDarkFeaturesStorage;
4   import com.atlassian.sal.api.pluginsettings.PluginSettings;
5   import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
6   import com.atlassian.util.concurrent.ResettableLazyReference;
7   import com.google.common.base.Function;
8   import com.google.common.collect.ImmutableSet;
9   import org.apache.commons.lang.StringUtils;
10  
11  import javax.annotation.Nullable;
12  import java.util.ArrayList;
13  import java.util.LinkedList;
14  import java.util.List;
15  
16  import static com.google.common.base.Preconditions.checkNotNull;
17  
18  /**
19   * Default implementation responsible for persisting site wide enabled dark features. The general contract is that
20   * reading is fast while updating is more expensive. Uses the plugin settings to store the dark features. Should be
21   * able to store up to 1.980 unique dark feature keys with each key about 50 characters long.
22   *
23   * @since 2.10
24   */
25  public class DefaultSiteDarkFeaturesStorage implements SiteDarkFeaturesStorage
26  {
27      private static final String SITE_WIDE_DARK_FEATURES = "atlassian.sitewide.dark.features";
28  
29      private final ResettableLazyReference<ImmutableSet<String>> cache = new ResettableLazyReference<ImmutableSet<String>>()
30      {
31          @Override
32          protected ImmutableSet<String> create() throws Exception
33          {
34              return ImmutableSet.copyOf(load());
35          }
36      };
37  
38      private final PluginSettingsFactory pluginSettingsFactory;
39  
40      public DefaultSiteDarkFeaturesStorage(final PluginSettingsFactory pluginSettingsFactory)
41      {
42          this.pluginSettingsFactory = pluginSettingsFactory;
43      }
44  
45      @Override
46      public void enable(final String featureKey)
47      {
48          final String trimmedFeatureKey = checkNotNull(StringUtils.trimToNull(featureKey), "featureKey must not be blank");
49          if (!cache.get().contains(trimmedFeatureKey))
50          {
51              update(addFeatureKey(trimmedFeatureKey));
52              cache.reset();
53          }
54      }
55  
56      @Override
57      public void disable(final String featureKey)
58      {
59          final String trimmedFeatureKey = checkNotNull(StringUtils.trimToNull(featureKey), "featureKey must not be blank");
60          if (cache.get().contains(trimmedFeatureKey))
61          {
62              update(removeFeatureKey(trimmedFeatureKey));
63              cache.reset();
64          }
65      }
66  
67      @Override
68      public ImmutableSet<String> getEnabledDarkFeatures()
69      {
70          return cache.get();
71      }
72  
73      /**
74       * Update the list of stored dark feature keys. The workflow is:
75       * <ol>
76       *     <li>Load the old list from storage</li>
77       *     <li>Apply the transformation function</li>
78       *     <li>Store the new list</li>
79       * </ol>
80       * @param transformer the function to be applied on the exist list of enabled dark feature keys
81       */
82      private synchronized void update(final Function<List<String>, List<String>> transformer)
83      {
84          /**
85           * Using a function allows to defer the actual list manipulation to a point when the thread
86           * got the proper locks.
87           */
88          final List<String> storedFeatureKeys = load();
89          final List<String> updatedFeatureKeys = transformer.apply(storedFeatureKeys);
90          store(updatedFeatureKeys);
91      }
92  
93      private synchronized List<String> load()
94      {
95          final PluginSettings globalSettings = pluginSettingsFactory.createGlobalSettings();
96          final Object value = globalSettings.get(SITE_WIDE_DARK_FEATURES);
97          return extractFeatureKeys(value);
98      }
99  
100     private List<String> extractFeatureKeys(@Nullable final Object value)
101     {
102         final LinkedList<String> storedFeatureKeys = new LinkedList<String>();
103         if (value instanceof List)
104         {
105             final List list = List.class.cast(value);
106             for (final Object listItem : list)
107             {
108                 if (listItem instanceof String)
109                 {
110                     storedFeatureKeys.addLast(String.class.cast(listItem));
111                 }
112             }
113         }
114         return storedFeatureKeys;
115     }
116 
117     private synchronized void store(final List<String> updatedFeatureKeys)
118     {
119         final PluginSettings globalSettings = pluginSettingsFactory.createGlobalSettings();
120         globalSettings.put(SITE_WIDE_DARK_FEATURES, updatedFeatureKeys);
121     }
122 
123     private Function<List<String>, List<String>> addFeatureKey(final String featureKey)
124     {
125         return new Function<List<String>, List<String>>()
126         {
127             @Override
128             public List<String> apply(@Nullable final List<String> storedFeatureKeys)
129             {
130                 if (storedFeatureKeys == null)
131                 {
132                     return storedFeatureKeys;
133                 }
134 
135                 final List<String> result = new ArrayList<String>(storedFeatureKeys);
136                 if (!storedFeatureKeys.contains(featureKey))
137                 {
138                     result.add(featureKey);
139                 }
140                 return result;
141             }
142         };
143     }
144 
145     private Function<List<String>, List<String>> removeFeatureKey(final String featureKey)
146     {
147         return new Function<List<String>, List<String>>()
148         {
149             @Override
150             public List<String> apply(@Nullable final List<String> storedFeatureKeys)
151             {
152                 if (storedFeatureKeys == null)
153                 {
154                     return storedFeatureKeys;
155                 }
156 
157                 final List<String> result = new ArrayList<String>(storedFeatureKeys);
158                 result.remove(featureKey);
159                 return result;
160             }
161         };
162     }
163 }