001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2014 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * SonarQube is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU Lesser General Public
008     * License as published by the Free Software Foundation; either
009     * version 3 of the License, or (at your option) any later version.
010     *
011     * SonarQube is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * Lesser General Public License for more details.
015     *
016     * You should have received a copy of the GNU Lesser General Public License
017     * along with this program; if not, write to the Free Software Foundation,
018     * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019     */
020    package org.sonar.api.config;
021    
022    import com.google.common.base.Joiner;
023    import com.google.common.base.Splitter;
024    import com.google.common.base.Strings;
025    import com.google.common.collect.ImmutableMap;
026    import com.google.common.collect.Lists;
027    import com.google.common.collect.Maps;
028    import org.apache.commons.lang.ArrayUtils;
029    import org.apache.commons.lang.StringUtils;
030    import org.sonar.api.BatchComponent;
031    import org.sonar.api.ServerComponent;
032    import org.sonar.api.utils.DateUtils;
033    
034    import javax.annotation.Nullable;
035    
036    import java.util.Date;
037    import java.util.List;
038    import java.util.Map;
039    import java.util.Properties;
040    
041    /**
042     * Project settings on batch side, or global settings on server side. This component does not access to database, so
043     * property changed via setter methods are not persisted.
044     * <p/>
045     * <p>
046     * For testing, you can create a new empty {@link Settings} component using {@link #Settings()} and then
047     * populate it using all variant of {@code setProperty}. <br/>
048     * If you want to test with default values of your properties taken into account there are two ways dependening on how you declare your properties.
049     * <ul>
050     * <li>If you are using annotations like:
051     * <pre>
052     * <code>{@literal @}Properties({
053     *   {@literal @}Property(
054     *     key = "sonar.myProp",
055     *     defaultValue = "A default value",
056     *     name = "My property"),
057     * })
058     * public class MyPlugin extends SonarPlugin {
059     * </code>
060     * </pre>
061     * then you can use:
062     * <pre>
063     * <code>new Settings(new PropertyDefinitions(MyPlugin.class))
064     * </code>
065     * </pre>
066     * </li>
067     * <li>If you are using the {@link PropertyDefinition#builder(String)} way like:
068     * <pre>
069     * <code>
070     * public class MyPlugin extends SonarPlugin {
071     *     public List getExtensions() {
072     *       return Arrays.asList(
073     *         PropertyDefinition.builder("sonar.myProp").name("My property").defaultValue("A default value").build()
074     *       );
075     *     }
076     *   }
077     * </code>
078     * </pre>
079     * then you can use:
080     * <pre>
081     * <code>new Settings(new PropertyDefinitions(new MyPlugin().getExtensions()))
082     * </code>
083     * </pre>
084     * </li>
085     * </p>
086     * @since 2.12
087     */
088    public class Settings implements BatchComponent, ServerComponent {
089    
090      protected Map<String, String> properties;
091      protected PropertyDefinitions definitions;
092      private Encryption encryption;
093    
094      public Settings() {
095        this(new PropertyDefinitions());
096      }
097    
098      public Settings(PropertyDefinitions definitions) {
099        this.properties = Maps.newHashMap();
100        this.definitions = definitions;
101        this.encryption = new Encryption(null);
102      }
103    
104      /**
105       * Clone settings. Actions are not propagated to cloned settings.
106       *
107       * @since 3.1
108       */
109      public Settings(Settings other) {
110        this.properties = Maps.newHashMap(other.properties);
111        this.definitions = other.definitions;
112        this.encryption = other.encryption;
113      }
114    
115      public Encryption getEncryption() {
116        return encryption;
117      }
118    
119      public String getDefaultValue(String key) {
120        return definitions.getDefaultValue(key);
121      }
122    
123      public boolean hasKey(String key) {
124        return properties.containsKey(key);
125      }
126    
127      public boolean hasDefaultValue(String key) {
128        return StringUtils.isNotEmpty(getDefaultValue(key));
129      }
130    
131      public String getString(String key) {
132        String value = getClearString(key);
133        if (value != null && encryption.isEncrypted(value)) {
134          try {
135            value = encryption.decrypt(value);
136          } catch (Exception e) {
137            throw new IllegalStateException("Fail to decrypt the property " + key + ". Please check your secret key.", e);
138          }
139        }
140        return value;
141      }
142    
143      /**
144       * Does not decrypt value.
145       */
146      protected String getClearString(String key) {
147        doOnGetProperties(key);
148        String validKey = definitions.validKey(key);
149        String value = properties.get(validKey);
150        if (value == null) {
151          value = getDefaultValue(validKey);
152        }
153        return value;
154      }
155    
156      public boolean getBoolean(String key) {
157        String value = getString(key);
158        return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value);
159      }
160    
161      /**
162       * @return the value as int. If the property does not exist and has no default value, then 0 is returned.
163       */
164      public int getInt(String key) {
165        String value = getString(key);
166        if (StringUtils.isNotEmpty(value)) {
167          return Integer.parseInt(value);
168        }
169        return 0;
170      }
171    
172      public long getLong(String key) {
173        String value = getString(key);
174        if (StringUtils.isNotEmpty(value)) {
175          return Long.parseLong(value);
176        }
177        return 0L;
178      }
179    
180      public Date getDate(String key) {
181        String value = getString(key);
182        if (StringUtils.isNotEmpty(value)) {
183          return DateUtils.parseDate(value);
184        }
185        return null;
186      }
187    
188      public Date getDateTime(String key) {
189        String value = getString(key);
190        if (StringUtils.isNotEmpty(value)) {
191          return DateUtils.parseDateTime(value);
192        }
193        return null;
194      }
195    
196      public Float getFloat(String key) {
197        String value = getString(key);
198        if (StringUtils.isNotEmpty(value)) {
199          try {
200            return Float.valueOf(value);
201          } catch (NumberFormatException e) {
202            throw new IllegalStateException(String.format("The property '%s' is not a float value", key));
203          }
204        }
205        return null;
206      }
207    
208      public Double getDouble(String key) {
209        String value = getString(key);
210        if (StringUtils.isNotEmpty(value)) {
211          try {
212            return Double.valueOf(value);
213          } catch (NumberFormatException e) {
214            throw new IllegalStateException(String.format("The property '%s' is not a double value", key));
215          }
216        }
217        return null;
218      }
219    
220      /**
221       * Value is split by comma and trimmed. Never returns null.
222       * <p/>
223       * Examples :
224       * <ul>
225       * <li>"one,two,three " -> ["one", "two", "three"]</li>
226       * <li>"  one, two, three " -> ["one", "two", "three"]</li>
227       * <li>"one, , three" -> ["one", "", "three"]</li>
228       * </ul>
229       */
230      public String[] getStringArray(String key) {
231        PropertyDefinition property = getDefinitions().get(key);
232        if ((null != property) && (property.multiValues())) {
233          String value = getString(key);
234          if (value == null) {
235            return ArrayUtils.EMPTY_STRING_ARRAY;
236          }
237    
238          List<String> values = Lists.newArrayList();
239          for (String v : Splitter.on(",").trimResults().split(value)) {
240            values.add(v.replace("%2C", ","));
241          }
242          return values.toArray(new String[values.size()]);
243        }
244    
245        return getStringArrayBySeparator(key, ",");
246      }
247    
248      /**
249       * Value is split by carriage returns.
250       *
251       * @return non-null array of lines. The line termination characters are excluded.
252       * @since 3.2
253       */
254      public String[] getStringLines(String key) {
255        String value = getString(key);
256        if (Strings.isNullOrEmpty(value)) {
257          return ArrayUtils.EMPTY_STRING_ARRAY;
258        }
259        return value.split("\r?\n|\r", -1);
260      }
261    
262      /**
263       * Value is splitted and trimmed.
264       */
265      public String[] getStringArrayBySeparator(String key, String separator) {
266        String value = getString(key);
267        if (value != null) {
268          String[] strings = StringUtils.splitByWholeSeparator(value, separator);
269          String[] result = new String[strings.length];
270          for (int index = 0; index < strings.length; index++) {
271            result[index] = StringUtils.trim(strings[index]);
272          }
273          return result;
274        }
275        return ArrayUtils.EMPTY_STRING_ARRAY;
276      }
277    
278      public List<String> getKeysStartingWith(String prefix) {
279        List<String> result = Lists.newArrayList();
280        for (String key : properties.keySet()) {
281          if (StringUtils.startsWith(key, prefix)) {
282            result.add(key);
283          }
284        }
285        return result;
286      }
287    
288      public Settings appendProperty(String key, String value) {
289        String newValue = properties.get(definitions.validKey(key));
290        if (StringUtils.isEmpty(newValue)) {
291          newValue = StringUtils.trim(value);
292        } else {
293          newValue += "," + StringUtils.trim(value);
294        }
295        return setProperty(key, newValue);
296      }
297    
298      public Settings setProperty(String key, @Nullable String[] values) {
299        PropertyDefinition property = getDefinitions().get(key);
300        if ((null == property) || (!property.multiValues())) {
301          throw new IllegalStateException("Fail to set multiple values on a single value property " + key);
302        }
303    
304        String text = null;
305        if (values != null) {
306          List<String> escaped = Lists.newArrayList();
307          for (String value : values) {
308            if (null != value) {
309              escaped.add(value.replace(",", "%2C"));
310            } else {
311              escaped.add("");
312            }
313          }
314    
315          String escapedValue = Joiner.on(',').join(escaped);
316          text = StringUtils.trim(escapedValue);
317        }
318        return setProperty(key, text);
319      }
320    
321      public Settings setProperty(String key, @Nullable String value) {
322        String validKey = definitions.validKey(key);
323        if (value == null) {
324          properties.remove(validKey);
325          doOnRemoveProperty(validKey);
326        } else {
327          properties.put(validKey, StringUtils.trim(value));
328          doOnSetProperty(validKey, value);
329        }
330        return this;
331      }
332    
333      public Settings setProperty(String key, @Nullable Boolean value) {
334        return setProperty(key, value == null ? null : String.valueOf(value));
335      }
336    
337      public Settings setProperty(String key, @Nullable Integer value) {
338        return setProperty(key, value == null ? null : String.valueOf(value));
339      }
340    
341      public Settings setProperty(String key, @Nullable Long value) {
342        return setProperty(key, value == null ? null : String.valueOf(value));
343      }
344    
345      public Settings setProperty(String key, @Nullable Double value) {
346        return setProperty(key, value == null ? null : String.valueOf(value));
347      }
348    
349      public Settings setProperty(String key, @Nullable Float value) {
350        return setProperty(key, value == null ? null : String.valueOf(value));
351      }
352    
353      public Settings setProperty(String key, @Nullable Date date) {
354        return setProperty(key, date, false);
355      }
356    
357      public Settings addProperties(Map<String, String> props) {
358        for (Map.Entry<String, String> entry : props.entrySet()) {
359          setProperty(entry.getKey(), entry.getValue());
360        }
361        return this;
362      }
363    
364      public Settings addProperties(Properties props) {
365        for (Map.Entry<Object, Object> entry : props.entrySet()) {
366          setProperty(entry.getKey().toString(), entry.getValue().toString());
367        }
368        return this;
369      }
370    
371      /**
372       * @deprecated since 4.4 For embedding purpose all properties should be provided by the bootstrapper
373       */
374      @Deprecated
375      public Settings addSystemProperties() {
376        return addProperties(System.getProperties());
377      }
378    
379      /**
380       * @deprecated since 4.4 For embedding purpose all properties should be provided by the bootstrapper
381       */
382      @Deprecated
383      public Settings addEnvironmentVariables() {
384        return addProperties(System.getenv());
385      }
386    
387      public Settings setProperties(Map<String, String> props) {
388        clear();
389        return addProperties(props);
390      }
391    
392      public Settings setProperty(String key, @Nullable Date date, boolean includeTime) {
393        if (date == null) {
394          return removeProperty(key);
395        }
396        return setProperty(key, includeTime ? DateUtils.formatDateTime(date) : DateUtils.formatDate(date));
397      }
398    
399      public Settings removeProperty(String key) {
400        return setProperty(key, (String) null);
401      }
402    
403      public Settings clear() {
404        properties.clear();
405        doOnClearProperties();
406        return this;
407      }
408    
409      /**
410       * @return immutable properties
411       */
412      public Map<String, String> getProperties() {
413        return ImmutableMap.copyOf(properties);
414      }
415    
416      public PropertyDefinitions getDefinitions() {
417        return definitions;
418      }
419    
420      /**
421       * Create empty settings. Definition of available properties is loaded from the given annotated class.
422       * This method is usually used by unit tests.
423       */
424      public static Settings createForComponent(Object component) {
425        return new Settings(new PropertyDefinitions(component));
426      }
427    
428      protected void doOnSetProperty(String key, @Nullable String value) {
429        // can be overridden
430      }
431    
432      protected void doOnRemoveProperty(String key) {
433        // can be overridden
434      }
435    
436      protected void doOnClearProperties() {
437        // can be overridden
438      }
439    
440      protected void doOnGetProperties(String key) {
441        // can be overridden
442      }
443    }