]> source.dussan.org Git - sonarqube.git/blob
97b76dc86629dc3f1de84e3437254708a0fe6ebc
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2017 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 package org.sonar.server.qualityprofile;
21
22 import com.google.common.collect.ArrayListMultimap;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.ListMultimap;
25 import com.google.common.collect.Multimaps;
26 import java.security.MessageDigest;
27 import java.util.Collection;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.Set;
34 import javax.annotation.Nullable;
35 import org.apache.commons.codec.digest.DigestUtils;
36 import org.sonar.api.profiles.ProfileDefinition;
37 import org.sonar.api.profiles.RulesProfile;
38 import org.sonar.api.resources.Languages;
39 import org.sonar.api.utils.ValidationMessages;
40 import org.sonar.api.utils.log.Logger;
41 import org.sonar.api.utils.log.Loggers;
42 import org.sonar.api.utils.log.Profiler;
43 import org.sonar.core.util.stream.MoreCollectors;
44
45 import static com.google.common.base.Preconditions.checkArgument;
46 import static com.google.common.base.Preconditions.checkState;
47 import static org.apache.commons.lang.StringUtils.isNotEmpty;
48 import static org.apache.commons.lang.StringUtils.lowerCase;
49
50 public class DefinedQProfileRepositoryImpl implements DefinedQProfileRepository {
51   private static final Logger LOGGER = Loggers.get(DefinedQProfileRepositoryImpl.class);
52   private static final String DEFAULT_PROFILE_NAME = "Sonar way";
53
54   private final Languages languages;
55   private final List<ProfileDefinition> definitions;
56   private Map<String, List<DefinedQProfile>> qProfilesByLanguage;
57
58   /**
59    * Requires for pico container when no {@link ProfileDefinition} is defined at all
60    */
61   public DefinedQProfileRepositoryImpl(Languages languages) {
62     this(languages, new ProfileDefinition[0]);
63   }
64
65   public DefinedQProfileRepositoryImpl(Languages languages, ProfileDefinition... definitions) {
66     this.languages = languages;
67     this.definitions = ImmutableList.copyOf(definitions);
68   }
69
70   @Override
71   public void initialize() {
72     checkState(qProfilesByLanguage == null, "initialize must be called only once");
73
74     Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Load quality profiles");
75     ListMultimap<String, RulesProfile> rulesProfilesByLanguage = buildRulesProfilesByLanguage();
76     validateAndClean(rulesProfilesByLanguage);
77     this.qProfilesByLanguage = toQualityProfilesByLanguage(rulesProfilesByLanguage);
78     profiler.stopDebug();
79   }
80
81   @Override
82   public Map<String, List<DefinedQProfile>> getQProfilesByLanguage() {
83     checkState(qProfilesByLanguage != null, "initialize must be called first");
84
85     return qProfilesByLanguage;
86   }
87
88   /**
89    * @return profiles by language
90    */
91   private ListMultimap<String, RulesProfile> buildRulesProfilesByLanguage() {
92     ListMultimap<String, RulesProfile> byLang = ArrayListMultimap.create();
93     for (ProfileDefinition definition : definitions) {
94       ValidationMessages validation = ValidationMessages.create();
95       RulesProfile profile = definition.createProfile(validation);
96       validation.log(LOGGER);
97       if (profile != null && !validation.hasErrors()) {
98         checkArgument(isNotEmpty(profile.getName()), "Profile created by Definition %s can't have a blank name", definition);
99         byLang.put(lowerCase(profile.getLanguage(), Locale.ENGLISH), profile);
100       }
101     }
102     return byLang;
103   }
104
105   private void validateAndClean(ListMultimap<String, RulesProfile> byLang) {
106     byLang.asMap().entrySet()
107       .removeIf(entry -> {
108         String language = entry.getKey();
109         if (languages.get(language) == null) {
110           LOGGER.info("Language {} is not installed, related Quality profiles are ignored", language);
111           return true;
112         }
113         Collection<RulesProfile> profiles = entry.getValue();
114         if (profiles.isEmpty()) {
115           LOGGER.warn("No Quality profiles defined for language: {}", language);
116           return true;
117         }
118         return false;
119       });
120   }
121
122   private static Map<String, List<DefinedQProfile>> toQualityProfilesByLanguage(ListMultimap<String, RulesProfile> rulesProfilesByLanguage) {
123     Map<String, List<DefinedQProfile.Builder>> buildersByLanguage = Multimaps.asMap(rulesProfilesByLanguage)
124       .entrySet()
125       .stream()
126       .collect(MoreCollectors.uniqueIndex(Map.Entry::getKey, DefinedQProfileRepositoryImpl::toQualityProfileBuilders));
127     return buildersByLanguage
128       .entrySet()
129       .stream()
130       .filter(DefinedQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault)
131       .collect(MoreCollectors.uniqueIndex(Map.Entry::getKey, entry -> toQualityProfiles(entry.getValue()), buildersByLanguage.size()));
132   }
133
134   /**
135    * Creates {@link DefinedQProfile.Builder} for each unique quality profile name for a given language.
136    * Builders will have the following properties populated:
137    * <ul>
138    *   <li>{@link DefinedQProfile.Builder#language language}: key of the method's parameter</li>
139    *   <li>{@link DefinedQProfile.Builder#name name}: {@link RulesProfile#getName()}</li>
140    *   <li>{@link DefinedQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile
141    *       with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}</li>
142    *   <li>{@link DefinedQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all
143    *       RulesProfile with a given name</li>
144    * </ul>
145    */
146   private static List<DefinedQProfile.Builder> toQualityProfileBuilders(Map.Entry<String, List<RulesProfile>> rulesProfilesByLanguage) {
147     String language = rulesProfilesByLanguage.getKey();
148     // use a LinkedHashMap to keep order of insertion of RulesProfiles
149     Map<String, DefinedQProfile.Builder> qualityProfileBuildersByName = new LinkedHashMap<>();
150     for (RulesProfile rulesProfile : rulesProfilesByLanguage.getValue()) {
151       qualityProfileBuildersByName.compute(
152         rulesProfile.getName(),
153         (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, rulesProfile, name));
154     }
155     return ImmutableList.copyOf(qualityProfileBuildersByName.values());
156   }
157
158   /**
159    * Fails if more than one {@link DefinedQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}.
160    */
161   private static boolean ensureAtMostOneDeclaredDefault(Map.Entry<String, List<DefinedQProfile.Builder>> entry) {
162     Set<String> declaredDefaultProfileNames = entry.getValue().stream()
163       .filter(DefinedQProfile.Builder::isDeclaredDefault)
164       .map(DefinedQProfile.Builder::getName)
165       .collect(MoreCollectors.toSet());
166     checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames);
167     return true;
168   }
169
170   private static DefinedQProfile.Builder updateOrCreateBuilder(String language, @Nullable DefinedQProfile.Builder existingBuilder, RulesProfile rulesProfile, String name) {
171     DefinedQProfile.Builder builder = existingBuilder;
172     if (builder == null) {
173       builder = new DefinedQProfile.Builder()
174         .setLanguage(language)
175         .setName(name);
176     }
177     Boolean defaultProfile = rulesProfile.getDefaultProfile();
178     boolean declaredDefault = defaultProfile != null && defaultProfile;
179     return builder
180       // if there is multiple RulesProfiles with the same name, if at least one is declared default,
181       // then QualityProfile is flagged as declared default
182       .setDeclaredDefault(builder.isDeclaredDefault() || declaredDefault)
183       .addRules(rulesProfile.getActiveRules());
184   }
185
186   private static List<DefinedQProfile> toQualityProfiles(List<DefinedQProfile.Builder> builders) {
187     if (builders.stream().noneMatch(DefinedQProfile.Builder::isDeclaredDefault)) {
188       Optional<DefinedQProfile.Builder> sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst();
189       if (sonarWayProfile.isPresent()) {
190         sonarWayProfile.get().setComputedDefault(true);
191       } else {
192         builders.iterator().next().setComputedDefault(true);
193       }
194     }
195     MessageDigest md5Digest = DigestUtils.getMd5Digest();
196     return builders.stream()
197       .map(builder -> builder.build(md5Digest))
198       .collect(MoreCollectors.toList(builders.size()));
199   }
200 }