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 */
020package org.sonar.batch.index;
021
022import com.google.common.collect.Lists;
023import com.google.common.collect.Maps;
024import com.google.common.collect.Sets;
025import org.apache.commons.lang.ObjectUtils;
026import org.apache.commons.lang.StringUtils;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029import org.sonar.api.batch.Event;
030import org.sonar.api.batch.SonarIndex;
031import org.sonar.api.database.model.ResourceModel;
032import org.sonar.api.design.Dependency;
033import org.sonar.api.measures.*;
034import org.sonar.api.profiles.RulesProfile;
035import org.sonar.api.resources.*;
036import org.sonar.api.rules.ActiveRule;
037import org.sonar.api.rules.Violation;
038import org.sonar.api.utils.SonarException;
039import org.sonar.api.violations.ViolationQuery;
040import org.sonar.batch.DefaultResourceCreationLock;
041import org.sonar.batch.ProjectTree;
042import org.sonar.batch.ResourceFilters;
043import org.sonar.batch.ViolationFilters;
044
045import java.util.*;
046
047public class DefaultIndex extends SonarIndex {
048
049  private static final Logger LOG = LoggerFactory.getLogger(DefaultIndex.class);
050
051  private RulesProfile profile;
052  private PersistenceManager persistence;
053  private DefaultResourceCreationLock lock;
054  private MetricFinder metricFinder;
055
056  // filters
057  private ViolationFilters violationFilters;
058  private ResourceFilters resourceFilters;
059
060  // caches
061  private Project currentProject;
062  private Map<Resource, Bucket> buckets = Maps.newHashMap();
063  private Set<Dependency> dependencies = Sets.newHashSet();
064  private Map<Resource, Map<Resource, Dependency>> outgoingDependenciesByResource = Maps.newHashMap();
065  private Map<Resource, Map<Resource, Dependency>> incomingDependenciesByResource = Maps.newHashMap();
066  private ProjectTree projectTree;
067
068  public DefaultIndex(PersistenceManager persistence, DefaultResourceCreationLock lock, ProjectTree projectTree, MetricFinder metricFinder) {
069    this.persistence = persistence;
070    this.lock = lock;
071    this.projectTree = projectTree;
072    this.metricFinder = metricFinder;
073  }
074
075  public void start() {
076    Project rootProject = projectTree.getRootProject();
077    doStart(rootProject);
078  }
079
080  void doStart(Project rootProject) {
081    Bucket bucket = new Bucket(rootProject);
082    buckets.put(rootProject, bucket);
083    persistence.saveProject(rootProject, null);
084    currentProject = rootProject;
085
086    for (Project project : rootProject.getModules()) {
087      addProject(project);
088    }
089  }
090
091  private void addProject(Project project) {
092    addResource(project);
093    for (Project module : project.getModules()) {
094      addProject(module);
095    }
096  }
097
098  @Override
099  public Project getProject() {
100    return currentProject;
101  }
102
103  public void setCurrentProject(Project project, ResourceFilters resourceFilters, ViolationFilters violationFilters, RulesProfile profile) {
104    this.currentProject = project;
105
106    // the following components depend on the current project, so they need to be reloaded.
107    this.resourceFilters = resourceFilters;
108    this.violationFilters = violationFilters;
109    this.profile = profile;
110  }
111
112  /**
113   * Keep only project stuff
114   */
115  public void clear() {
116    Iterator<Map.Entry<Resource, Bucket>> it = buckets.entrySet().iterator();
117    while (it.hasNext()) {
118      Map.Entry<Resource, Bucket> entry = it.next();
119      Resource resource = entry.getKey();
120      if (!ResourceUtils.isSet(resource)) {
121        entry.getValue().clear();
122        it.remove();
123      }
124    }
125
126    Set<Dependency> projectDependencies = getDependenciesBetweenProjects();
127    dependencies.clear();
128    incomingDependenciesByResource.clear();
129    outgoingDependenciesByResource.clear();
130    for (Dependency projectDependency : projectDependencies) {
131      projectDependency.setId(null);
132      registerDependency(projectDependency);
133    }
134
135    lock.unlock();
136  }
137
138  @Override
139  public Measure getMeasure(Resource resource, Metric metric) {
140    Bucket bucket = buckets.get(resource);
141    if (bucket != null) {
142      Measure measure = bucket.getMeasures(MeasuresFilters.metric(metric));
143      if (measure != null) {
144        return persistence.reloadMeasure(measure);
145      }
146    }
147    return null;
148  }
149
150  @Override
151  public <M> M getMeasures(Resource resource, MeasuresFilter<M> filter) {
152    Bucket bucket = buckets.get(resource);
153    if (bucket != null) {
154      // TODO the data measures which are not kept in memory are not reloaded yet. Use getMeasure().
155      return bucket.getMeasures(filter);
156    }
157    return null;
158  }
159
160  /**
161   * the measure is updated if it's already registered.
162   */
163  @Override
164  public Measure addMeasure(Resource resource, Measure measure) {
165    Bucket bucket = checkIndexed(resource);
166    if (bucket != null && !bucket.isExcluded()) {
167      Metric metric = metricFinder.findByKey(measure.getMetricKey());
168      if (metric == null) {
169        throw new SonarException("Unknown metric: " + measure.getMetricKey());
170      }
171      measure.setMetric(metric);
172      bucket.addMeasure(measure);
173
174      if (measure.getPersistenceMode().useDatabase()) {
175        persistence.saveMeasure(resource, measure);
176      }
177    }
178    return measure;
179  }
180
181  //
182  //
183  //
184  // DEPENDENCIES
185  //
186  //
187  //
188
189  @Override
190  public Dependency addDependency(Dependency dependency) {
191    Dependency existingDep = getEdge(dependency.getFrom(), dependency.getTo());
192    if (existingDep != null) {
193      return existingDep;
194    }
195
196    Dependency parentDependency = dependency.getParent();
197    if (parentDependency != null) {
198      addDependency(parentDependency);
199    }
200
201    if (registerDependency(dependency)) {
202      persistence.saveDependency(currentProject, dependency, parentDependency);
203    }
204    return dependency;
205  }
206
207  boolean registerDependency(Dependency dependency) {
208    Bucket fromBucket = doIndex(dependency.getFrom());
209    Bucket toBucket = doIndex(dependency.getTo());
210
211    if (fromBucket != null && !fromBucket.isExcluded() && toBucket != null && !toBucket.isExcluded()) {
212      dependencies.add(dependency);
213      registerOutgoingDependency(dependency);
214      registerIncomingDependency(dependency);
215      return true;
216    }
217    return false;
218  }
219
220  private void registerOutgoingDependency(Dependency dependency) {
221    Map<Resource, Dependency> outgoingDeps = outgoingDependenciesByResource.get(dependency.getFrom());
222    if (outgoingDeps == null) {
223      outgoingDeps = new HashMap<Resource, Dependency>();
224      outgoingDependenciesByResource.put(dependency.getFrom(), outgoingDeps);
225    }
226    outgoingDeps.put(dependency.getTo(), dependency);
227  }
228
229  private void registerIncomingDependency(Dependency dependency) {
230    Map<Resource, Dependency> incomingDeps = incomingDependenciesByResource.get(dependency.getTo());
231    if (incomingDeps == null) {
232      incomingDeps = new HashMap<Resource, Dependency>();
233      incomingDependenciesByResource.put(dependency.getTo(), incomingDeps);
234    }
235    incomingDeps.put(dependency.getFrom(), dependency);
236  }
237
238  @Override
239  public Set<Dependency> getDependencies() {
240    return dependencies;
241  }
242
243  public Dependency getEdge(Resource from, Resource to) {
244    Map<Resource, Dependency> map = outgoingDependenciesByResource.get(from);
245    if (map != null) {
246      return map.get(to);
247    }
248    return null;
249  }
250
251  public boolean hasEdge(Resource from, Resource to) {
252    return getEdge(from, to) != null;
253  }
254
255  public Set<Resource> getVertices() {
256    return buckets.keySet();
257  }
258
259  public Collection<Dependency> getOutgoingEdges(Resource from) {
260    Map<Resource, Dependency> deps = outgoingDependenciesByResource.get(from);
261    if (deps != null) {
262      return deps.values();
263    }
264    return Collections.emptyList();
265  }
266
267  public Collection<Dependency> getIncomingEdges(Resource to) {
268    Map<Resource, Dependency> deps = incomingDependenciesByResource.get(to);
269    if (deps != null) {
270      return deps.values();
271    }
272    return Collections.emptyList();
273  }
274
275  Set<Dependency> getDependenciesBetweenProjects() {
276    Set<Dependency> result = Sets.newLinkedHashSet();
277    for (Dependency dependency : dependencies) {
278      if (ResourceUtils.isSet(dependency.getFrom()) || ResourceUtils.isSet(dependency.getTo())) {
279        result.add(dependency);
280      }
281    }
282    return result;
283  }
284
285  //
286  //
287  //
288  // VIOLATIONS
289  //
290  //
291  //
292
293  /**
294   * {@inheritDoc}
295   */
296  @Override
297  public List<Violation> getViolations(ViolationQuery violationQuery) {
298    Resource resource = violationQuery.getResource();
299    if (resource == null) {
300      throw new IllegalArgumentException("A resource must be set on the ViolationQuery in order to search for violations.");
301    }
302    Bucket bucket = buckets.get(resource);
303    if (bucket == null) {
304      return Collections.emptyList();
305    }
306    List<Violation> filteredViolations = Lists.newArrayList();
307    ViolationQuery.SwitchMode mode = violationQuery.getSwitchMode();
308    for (Violation violation : bucket.getViolations()) {
309      if (mode == ViolationQuery.SwitchMode.BOTH ||
310          (mode == ViolationQuery.SwitchMode.OFF && violation.isSwitchedOff()) ||
311          (mode == ViolationQuery.SwitchMode.ON && !violation.isSwitchedOff())) {
312        filteredViolations.add(violation);
313      }
314    }
315    return filteredViolations;
316  }
317
318  @Override
319  public void addViolation(Violation violation, boolean force) {
320    Resource resource = violation.getResource();
321    if (resource == null) {
322      violation.setResource(currentProject);
323    } else if (!Scopes.isHigherThanOrEquals(resource, Scopes.FILE)) {
324      throw new IllegalArgumentException("Violations are only supported on files, directories and project");
325    }
326
327    if (violation.getRule() == null) {
328      LOG.warn("Rule is null, ignoring violation {}", violation);
329      return;
330    }
331
332    Bucket bucket = checkIndexed(resource);
333    if (bucket != null && !bucket.isExcluded()) {
334      boolean isIgnored = !force && violationFilters != null && violationFilters.isIgnored(violation);
335      if (!isIgnored) {
336
337        // TODO this code is not the responsibility of this index. It should be moved somewhere else.
338
339        if (violation.isManual()) {
340          doAddViolation(violation, bucket);
341        } else {
342          ActiveRule activeRule = profile.getActiveRule(violation.getRule());
343          if (activeRule == null) {
344            if (currentProject.getReuseExistingRulesConfig()) {
345              violation.setSeverity(violation.getRule().getSeverity());
346              doAddViolation(violation, bucket);
347
348            } else {
349              LoggerFactory.getLogger(getClass()).debug("Rule is not activated, ignoring violation {}", violation);
350            }
351
352          } else {
353            violation.setSeverity(activeRule.getSeverity());
354            doAddViolation(violation, bucket);
355          }
356        }
357      }
358    }
359  }
360
361  private void doAddViolation(Violation violation, Bucket bucket) {
362    bucket.addViolation(violation);
363  }
364
365  //
366  //
367  //
368  // LINKS
369  //
370  //
371  //
372
373  @Override
374  public void addLink(ProjectLink link) {
375    persistence.saveLink(currentProject, link);
376  }
377
378  @Override
379  public void deleteLink(String key) {
380    persistence.deleteLink(currentProject, key);
381  }
382
383  //
384  //
385  //
386  // EVENTS
387  //
388  //
389  //
390
391  @Override
392  public List<Event> getEvents(Resource resource) {
393    // currently events are not cached in memory
394    return persistence.getEvents(resource);
395  }
396
397  @Override
398  public void deleteEvent(Event event) {
399    persistence.deleteEvent(event);
400  }
401
402  @Override
403  public Event addEvent(Resource resource, String name, String description, String category, Date date) {
404    Event event = new Event(name, description, category);
405    event.setDate(date);
406    event.setCreatedAt(new Date());
407
408    persistence.saveEvent(resource, event);
409    return null;
410  }
411
412  @Override
413  public void setSource(Resource reference, String source) {
414    Bucket bucket = checkIndexed(reference);
415    if (bucket != null && !bucket.isExcluded()) {
416      persistence.setSource(reference, source);
417    }
418  }
419
420  @Override
421  public String getSource(Resource resource) {
422    return persistence.getSource(resource);
423  }
424
425  /**
426   * Does nothing if the resource is already registered.
427   */
428  @Override
429  public Resource addResource(Resource resource) {
430    Bucket bucket = doIndex(resource);
431    return bucket != null ? bucket.getResource() : null;
432  }
433
434  @Override
435  public <R extends Resource> R getResource(R reference) {
436    Bucket bucket = buckets.get(reference);
437    if (bucket != null) {
438      return (R) bucket.getResource();
439    }
440    return null;
441  }
442
443  static String createUID(Project project, Resource resource) {
444    String uid = resource.getKey();
445    if (!StringUtils.equals(Scopes.PROJECT, resource.getScope())) {
446      // not a project nor a library
447      uid = new StringBuilder(ResourceModel.KEY_SIZE)
448          .append(project.getKey())
449          .append(':')
450          .append(resource.getKey())
451          .toString();
452    }
453    return uid;
454  }
455
456  private boolean checkExclusion(Resource resource, Bucket parent) {
457    boolean excluded = (parent != null && parent.isExcluded()) || (resourceFilters != null && resourceFilters.isExcluded(resource));
458    resource.setExcluded(excluded);
459    return excluded;
460  }
461
462  @Override
463  public List<Resource> getChildren(Resource resource) {
464    return getChildren(resource, false);
465  }
466
467  public List<Resource> getChildren(Resource resource, boolean acceptExcluded) {
468    List<Resource> children = Lists.newLinkedList();
469    Bucket bucket = getBucket(resource, acceptExcluded);
470    if (bucket != null) {
471      for (Bucket childBucket : bucket.getChildren()) {
472        if (acceptExcluded || !childBucket.isExcluded()) {
473          children.add(childBucket.getResource());
474        }
475      }
476    }
477    return children;
478  }
479
480  @Override
481  public Resource getParent(Resource resource) {
482    Bucket bucket = getBucket(resource, false);
483    if (bucket != null && bucket.getParent() != null) {
484      return bucket.getParent().getResource();
485    }
486    return null;
487  }
488
489  @Override
490  public boolean index(Resource resource) {
491    Bucket bucket = doIndex(resource);
492    return bucket != null && !bucket.isExcluded();
493  }
494
495  private Bucket doIndex(Resource resource) {
496    if (resource.getParent() != null) {
497      doIndex(resource.getParent());
498    }
499    return doIndex(resource, resource.getParent());
500  }
501
502  @Override
503  public boolean index(Resource resource, Resource parentReference) {
504    Bucket bucket = doIndex(resource, parentReference);
505    return bucket != null && !bucket.isExcluded();
506  }
507
508  private Bucket doIndex(Resource resource, Resource parentReference) {
509    Bucket bucket = buckets.get(resource);
510    if (bucket != null) {
511      return bucket;
512    }
513
514    checkLock(resource);
515
516    Resource parent = null;
517    if (!ResourceUtils.isLibrary(resource)) {
518      // a library has no parent
519      parent = (Resource) ObjectUtils.defaultIfNull(parentReference, currentProject);
520    }
521
522    Bucket parentBucket = getBucket(parent, true);
523    if (parentBucket == null && parent != null) {
524      LOG.warn("Resource ignored, parent is not indexed: " + resource);
525      return null;
526    }
527
528    resource.setEffectiveKey(createUID(currentProject, resource));
529    bucket = new Bucket(resource).setParent(parentBucket);
530    buckets.put(resource, bucket);
531
532    boolean excluded = checkExclusion(resource, parentBucket);
533    if (!excluded) {
534      persistence.saveResource(currentProject, resource, (parentBucket != null ? parentBucket.getResource() : null));
535    }
536    return bucket;
537  }
538
539  private void checkLock(Resource resource) {
540    if (lock.isLocked() && !ResourceUtils.isLibrary(resource)) {
541      if (lock.isFailWhenLocked()) {
542        throw new SonarException("Index is locked, resource can not be indexed: " + resource);
543      }
544    }
545  }
546
547  private Bucket checkIndexed(Resource resource) {
548    Bucket bucket = getBucket(resource, true);
549    if (bucket == null) {
550      if (lock.isLocked()) {
551        if (lock.isFailWhenLocked()) {
552          throw new ResourceNotIndexedException(resource);
553        }
554        LOG.warn("Resource will be ignored in next Sonar versions, index is locked: " + resource);
555      }
556      if (Scopes.isDirectory(resource) || Scopes.isFile(resource)) {
557        bucket = doIndex(resource);
558      } else if (!lock.isLocked()) {
559        LOG.warn("Resource will be ignored in next Sonar versions, it must be indexed before adding data: " + resource);
560      }
561    }
562    return bucket;
563  }
564
565  @Override
566  public boolean isExcluded(Resource reference) {
567    Bucket bucket = getBucket(reference, true);
568    return bucket != null && bucket.isExcluded();
569  }
570
571  @Override
572  public boolean isIndexed(Resource reference, boolean acceptExcluded) {
573    return getBucket(reference, acceptExcluded) != null;
574  }
575
576  private Bucket getBucket(Resource resource, boolean acceptExcluded) {
577    Bucket bucket = null;
578    if (resource != null) {
579      bucket = buckets.get(resource);
580      if (!acceptExcluded && bucket != null && bucket.isExcluded()) {
581        bucket = null;
582      }
583    }
584    return bucket;
585  }
586}