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