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.measures;
021    
022    import org.apache.commons.collections.SortedBag;
023    import org.apache.commons.collections.Transformer;
024    import org.apache.commons.collections.bag.TransformedSortedBag;
025    import org.apache.commons.collections.bag.TreeBag;
026    import org.apache.commons.lang.NumberUtils;
027    import org.sonar.api.utils.KeyValueFormat;
028    import org.sonar.api.utils.SonarException;
029    
030    import java.util.Arrays;
031    import java.util.Collections;
032    import java.util.Map;
033    import java.util.Set;
034    
035    /**
036     * Utility to build a distribution based on defined ranges
037     * <p/>
038     * <p>An example of usage : you wish to record the percentage of lines of code that belong to method
039     * with pre-defined ranges of complexity.</p>
040     *
041     * @since 1.10
042     */
043    public class RangeDistributionBuilder implements MeasureBuilder {
044    
045      private Metric<String> metric;
046      private SortedBag countBag;
047      private boolean isEmpty = true;
048      private Number[] bottomLimits;
049      private boolean isValid = true;
050    
051      /**
052       * RangeDistributionBuilder for a metric and a defined range
053       * Each entry is initialized at zero
054       *
055       * @param metric       the metric to record the measure against
056       * @param bottomLimits the bottom limits of ranges to be used
057       */
058      public RangeDistributionBuilder(Metric<String> metric, Number[] bottomLimits) {
059        setMetric(metric);
060        init(bottomLimits);
061      }
062    
063      private void init(Number[] bottomLimits) {
064        this.bottomLimits = new Number[bottomLimits.length];
065        System.arraycopy(bottomLimits, 0, this.bottomLimits, 0, this.bottomLimits.length);
066        Arrays.sort(this.bottomLimits);
067        changeDoublesToInts();
068        countBag = TransformedSortedBag.decorate(new TreeBag(), new RangeTransformer());
069        doClear();
070      }
071    
072      private void changeDoublesToInts() {
073        boolean onlyInts = true;
074        for (Number bottomLimit : bottomLimits) {
075          if (NumberUtils.compare(bottomLimit.intValue(), bottomLimit.doubleValue()) != 0) {
076            onlyInts = false;
077          }
078        }
079        if (onlyInts) {
080          for (int i = 0; i < bottomLimits.length; i++) {
081            bottomLimits[i] = bottomLimits[i].intValue();
082          }
083        }
084      }
085    
086      public RangeDistributionBuilder(Metric<String> metric) {
087        this.metric = metric;
088      }
089    
090      /**
091       * Gives the bottom limits of ranges used
092       *
093       * @return the bottom limits of defined range for the distribution
094       */
095      public Number[] getBottomLimits() {
096        return bottomLimits;
097      }
098    
099      /**
100       * Increments an entry by 1
101       *
102       * @param value the value to use to pick the entry to increment
103       * @return the current object
104       */
105      public RangeDistributionBuilder add(Number value) {
106        return add(value, 1);
107      }
108    
109      /**
110       * Increments an entry
111       *
112       * @param value the value to use to pick the entry to increment
113       * @param count the number by which to increment
114       * @return the current object
115       */
116      public RangeDistributionBuilder add(Number value, int count) {
117        if (value != null && greaterOrEqualsThan(value, bottomLimits[0])) {
118          this.countBag.add(value, count);
119          isEmpty = false;
120        }
121        return this;
122      }
123    
124      private RangeDistributionBuilder addLimitCount(Number limit, int count) {
125        for (Number bottomLimit : bottomLimits) {
126          if (NumberUtils.compare(bottomLimit.doubleValue(), limit.doubleValue()) == 0) {
127            this.countBag.add(limit, count);
128            isEmpty = false;
129            return this;
130          }
131        }
132        isValid = false;
133        return this;
134      }
135    
136      /**
137       * Adds an existing Distribution to the current one.
138       * It will create the entries if they don't exist.
139       * Can be used to add the values of children resources for example
140       * <p/>
141       * Since 2.2, the distribution returned will be invalidated in case the
142       * measure given does not use the same bottom limits
143       *
144       * @param measure the measure to add to the current one
145       * @return the current object
146       */
147      public RangeDistributionBuilder add(Measure<String> measure) {
148        if (measure != null && measure.getData() != null) {
149          Map<Double, Double> map = KeyValueFormat.parse(measure.getData(), new KeyValueFormat.DoubleNumbersPairTransformer());
150          Number[] limits = map.keySet().toArray(new Number[map.size()]);
151          if (bottomLimits == null) {
152            init(limits);
153    
154          } else if (!areSameLimits(bottomLimits, map.keySet())) {
155            isValid = false;
156          }
157    
158          if (isValid) {
159            for (Map.Entry<Double, Double> entry : map.entrySet()) {
160              addLimitCount(entry.getKey(), entry.getValue().intValue());
161            }
162          }
163        }
164        return this;
165      }
166    
167      private boolean areSameLimits(Number[] bottomLimits, Set<Double> limits) {
168        if (limits.size() == bottomLimits.length) {
169          for (Number l : bottomLimits) {
170            if (!limits.contains(l.doubleValue())) {
171              return false;
172            }
173          }
174          return true;
175        }
176        return false;
177      }
178    
179      /**
180       * Resets all entries to zero
181       *
182       * @return the current object
183       */
184      public RangeDistributionBuilder clear() {
185        doClear();
186        return this;
187      }
188    
189      private void doClear() {
190        if (countBag != null) {
191          countBag.clear();
192        }
193        if (bottomLimits != null) {
194          Collections.addAll(countBag, bottomLimits);
195        }
196        isEmpty = true;
197      }
198    
199      /**
200       * @return whether the current object is empty or not
201       */
202      public boolean isEmpty() {
203        return isEmpty;
204      }
205    
206      /**
207       * Shortcut for <code>build(true)</code>
208       *
209       * @return the built measure
210       */
211      public Measure<String> build() {
212        return build(true);
213      }
214    
215      /**
216       * Used to build a measure from the current object
217       *
218       * @param allowEmptyData should be built if current object is empty
219       * @return the built measure
220       */
221      public Measure<String> build(boolean allowEmptyData) {
222        if (isValid && (!isEmpty || allowEmptyData)) {
223          return new Measure<String>(metric, KeyValueFormat.format(countBag, -1));
224        }
225        return null;
226      }
227    
228      private class RangeTransformer implements Transformer {
229        public Object transform(Object o) {
230          Number n = (Number) o;
231          for (int i = bottomLimits.length - 1; i >= 0; i--) {
232            if (greaterOrEqualsThan(n, bottomLimits[i])) {
233              return bottomLimits[i];
234            }
235          }
236          return null;
237        }
238      }
239    
240      private static boolean greaterOrEqualsThan(Number n1, Number n2) {
241        return NumberUtils.compare(n1.doubleValue(), n2.doubleValue()) >= 0;
242      }
243    
244      private void setMetric(Metric metric) {
245        if (metric == null || !metric.isDataType()) {
246          throw new SonarException("Metric is null or has unvalid type");
247        }
248        this.metric = metric;
249      }
250    }