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