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      private static final String SITE_WIDE_DARK_FEATURES = "atlassian.sitewide.dark.features";
27  
28      private final ResettableLazyReference<ImmutableSet<String>> cache = new ResettableLazyReference<ImmutableSet<String>>() {
29          @Override
30          protected ImmutableSet<String> create() throws Exception {
31              return ImmutableSet.copyOf(load());
32          }
33      };
34  
35      private final PluginSettingsFactory pluginSettingsFactory;
36  
37      public DefaultSiteDarkFeaturesStorage(final PluginSettingsFactory pluginSettingsFactory) {
38          this.pluginSettingsFactory = pluginSettingsFactory;
39      }
40  
41      @Override
42      public void enable(final String featureKey) {
43          final String trimmedFeatureKey = checkNotNull(StringUtils.trimToNull(featureKey), "featureKey must not be blank");
44          if (!cache.get().contains(trimmedFeatureKey)) {
45              update(addFeatureKey(trimmedFeatureKey));
46              cache.reset();
47          }
48      }
49  
50      @Override
51      public void disable(final String featureKey) {
52          final String trimmedFeatureKey = checkNotNull(StringUtils.trimToNull(featureKey), "featureKey must not be blank");
53          if (cache.get().contains(trimmedFeatureKey)) {
54              update(removeFeatureKey(trimmedFeatureKey));
55              cache.reset();
56          }
57      }
58  
59      @Override
60      public ImmutableSet<String> getEnabledDarkFeatures() {
61          return cache.get();
62      }
63  
64      /**
65       * Update the list of stored dark feature keys. The workflow is:
66       * <ol>
67       * <li>Load the old list from storage</li>
68       * <li>Apply the transformation function</li>
69       * <li>Store the new list</li>
70       * </ol>
71       *
72       * @param transformer the function to be applied on the exist list of enabled dark feature keys
73       */
74      private synchronized void update(final Function<List<String>, List<String>> transformer) {
75          /**
76           * Using a function allows to defer the actual list manipulation to a point when the thread
77           * got the proper locks.
78           */
79          final List<String> storedFeatureKeys = load();
80          final List<String> updatedFeatureKeys = transformer.apply(storedFeatureKeys);
81          store(updatedFeatureKeys);
82      }
83  
84      private synchronized List<String> load() {
85          final PluginSettings globalSettings = pluginSettingsFactory.createGlobalSettings();
86          final Object value = globalSettings.get(SITE_WIDE_DARK_FEATURES);
87          return extractFeatureKeys(value);
88      }
89  
90      private List<String> extractFeatureKeys(@Nullable final Object value) {
91          final LinkedList<String> storedFeatureKeys = new LinkedList<String>();
92          if (value instanceof List) {
93              final List list = List.class.cast(value);
94              for (final Object listItem : list) {
95                  if (listItem instanceof String) {
96                      storedFeatureKeys.addLast(String.class.cast(listItem));
97                  }
98              }
99          }
100         return storedFeatureKeys;
101     }
102 
103     private synchronized void store(final List<String> updatedFeatureKeys) {
104         final PluginSettings globalSettings = pluginSettingsFactory.createGlobalSettings();
105         globalSettings.put(SITE_WIDE_DARK_FEATURES, updatedFeatureKeys);
106     }
107 
108     private Function<List<String>, List<String>> addFeatureKey(final String featureKey) {
109         return new Function<List<String>, List<String>>() {
110             @Override
111             public List<String> apply(@Nullable final List<String> storedFeatureKeys) {
112                 if (storedFeatureKeys == null) {
113                     return storedFeatureKeys;
114                 }
115 
116                 final List<String> result = new ArrayList<String>(storedFeatureKeys);
117                 if (!storedFeatureKeys.contains(featureKey)) {
118                     result.add(featureKey);
119                 }
120                 return result;
121             }
122         };
123     }
124 
125     private Function<List<String>, List<String>> removeFeatureKey(final String featureKey) {
126         return new Function<List<String>, List<String>>() {
127             @Override
128             public List<String> apply(@Nullable final List<String> storedFeatureKeys) {
129                 if (storedFeatureKeys == null) {
130                     return storedFeatureKeys;
131                 }
132 
133                 final List<String> result = new ArrayList<String>(storedFeatureKeys);
134                 result.remove(featureKey);
135                 return result;
136             }
137         };
138     }
139 }