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.plugins.cpd; 021 022 import com.google.common.collect.Iterables; 023 import org.apache.commons.io.IOUtils; 024 import org.slf4j.Logger; 025 import org.slf4j.LoggerFactory; 026 import org.sonar.api.batch.SensorContext; 027 import org.sonar.api.database.model.ResourceModel; 028 import org.sonar.api.measures.CoreMetrics; 029 import org.sonar.api.measures.Measure; 030 import org.sonar.api.measures.PersistenceMode; 031 import org.sonar.api.resources.*; 032 import org.sonar.api.utils.SonarException; 033 import org.sonar.duplications.block.Block; 034 import org.sonar.duplications.block.BlockChunker; 035 import org.sonar.duplications.detector.suffixtree.SuffixTreeCloneDetectionAlgorithm; 036 import org.sonar.duplications.index.CloneGroup; 037 import org.sonar.duplications.index.CloneIndex; 038 import org.sonar.duplications.index.ClonePart; 039 import org.sonar.duplications.java.JavaStatementBuilder; 040 import org.sonar.duplications.java.JavaTokenProducer; 041 import org.sonar.duplications.statement.Statement; 042 import org.sonar.duplications.statement.StatementChunker; 043 import org.sonar.duplications.token.TokenChunker; 044 import org.sonar.plugins.cpd.index.IndexFactory; 045 import org.sonar.plugins.cpd.index.SonarDuplicationsIndex; 046 047 import javax.annotation.Nullable; 048 049 import java.io.FileInputStream; 050 import java.io.FileNotFoundException; 051 import java.io.InputStreamReader; 052 import java.io.Reader; 053 import java.util.Collection; 054 import java.util.HashSet; 055 import java.util.List; 056 import java.util.Set; 057 import java.util.concurrent.*; 058 059 public class SonarEngine extends CpdEngine { 060 061 private static final Logger LOG = LoggerFactory.getLogger(SonarEngine.class); 062 063 private static final int BLOCK_SIZE = 10; 064 065 /** 066 * Limit of time to analyse one file (in seconds). 067 */ 068 private static final int TIMEOUT = 5 * 60; 069 070 private final IndexFactory indexFactory; 071 072 public SonarEngine(IndexFactory indexFactory) { 073 this.indexFactory = indexFactory; 074 } 075 076 @Override 077 public boolean isLanguageSupported(Language language) { 078 return Java.INSTANCE.equals(language); 079 } 080 081 static String getFullKey(Project project, Resource resource) { 082 return new StringBuilder(ResourceModel.KEY_SIZE) 083 .append(project.getKey()) 084 .append(':') 085 .append(resource.getKey()) 086 .toString(); 087 } 088 089 @Override 090 public void analyse(Project project, SensorContext context) { 091 List<InputFile> inputFiles = project.getFileSystem().mainFiles(project.getLanguageKey()); 092 if (inputFiles.isEmpty()) { 093 return; 094 } 095 SonarDuplicationsIndex index = createIndex(project, inputFiles); 096 detect(index, context, project, inputFiles); 097 } 098 099 private SonarDuplicationsIndex createIndex(Project project, List<InputFile> inputFiles) { 100 final SonarDuplicationsIndex index = indexFactory.create(project); 101 102 TokenChunker tokenChunker = JavaTokenProducer.build(); 103 StatementChunker statementChunker = JavaStatementBuilder.build(); 104 BlockChunker blockChunker = new BlockChunker(BLOCK_SIZE); 105 106 for (InputFile inputFile : inputFiles) { 107 LOG.debug("Populating index from {}", inputFile.getFile()); 108 Resource resource = getResource(inputFile); 109 String resourceKey = getFullKey(project, resource); 110 111 List<Statement> statements; 112 113 Reader reader = null; 114 try { 115 reader = new InputStreamReader(new FileInputStream(inputFile.getFile()), project.getFileSystem().getSourceCharset()); 116 statements = statementChunker.chunk(tokenChunker.chunk(reader)); 117 } catch (FileNotFoundException e) { 118 throw new SonarException(e); 119 } finally { 120 IOUtils.closeQuietly(reader); 121 } 122 123 List<Block> blocks = blockChunker.chunk(resourceKey, statements); 124 index.insert(resource, blocks); 125 } 126 127 return index; 128 } 129 130 private void detect(SonarDuplicationsIndex index, SensorContext context, Project project, List<InputFile> inputFiles) { 131 ExecutorService executorService = Executors.newSingleThreadExecutor(); 132 try { 133 for (InputFile inputFile : inputFiles) { 134 LOG.debug("Detection of duplications for {}", inputFile.getFile()); 135 Resource resource = getResource(inputFile); 136 String resourceKey = getFullKey(project, resource); 137 138 Collection<Block> fileBlocks = index.getByResource(resource, resourceKey); 139 140 List<CloneGroup> clones; 141 try { 142 clones = executorService.submit(new Task(index, fileBlocks)).get(TIMEOUT, TimeUnit.SECONDS); 143 } catch (TimeoutException e) { 144 clones = null; 145 LOG.warn("Timeout during detection of duplications for " + inputFile.getFile(), e); 146 } catch (InterruptedException e) { 147 throw new SonarException(e); 148 } catch (ExecutionException e) { 149 throw new SonarException(e); 150 } 151 152 save(context, resource, clones); 153 } 154 } finally { 155 executorService.shutdown(); 156 } 157 } 158 159 static class Task implements Callable<List<CloneGroup>> { 160 private final CloneIndex index; 161 private final Collection<Block> fileBlocks; 162 163 public Task(CloneIndex index, Collection<Block> fileBlocks) { 164 this.index = index; 165 this.fileBlocks = fileBlocks; 166 } 167 168 public List<CloneGroup> call() { 169 return SuffixTreeCloneDetectionAlgorithm.detect(index, fileBlocks); 170 } 171 } 172 173 private Resource getResource(InputFile inputFile) { 174 return JavaFile.fromRelativePath(inputFile.getRelativePath(), false); 175 } 176 177 static void save(SensorContext context, Resource resource, @Nullable Iterable<CloneGroup> duplications) { 178 if (duplications == null || Iterables.isEmpty(duplications)) { 179 return; 180 } 181 // Calculate number of lines and blocks 182 Set<Integer> duplicatedLines = new HashSet<Integer>(); 183 double duplicatedBlocks = 0; 184 for (CloneGroup clone : duplications) { 185 ClonePart origin = clone.getOriginPart(); 186 for (ClonePart part : clone.getCloneParts()) { 187 if (part.getResourceId().equals(origin.getResourceId())) { 188 duplicatedBlocks++; 189 for (int duplicatedLine = part.getStartLine(); duplicatedLine < part.getStartLine() + part.getLines(); duplicatedLine++) { 190 duplicatedLines.add(duplicatedLine); 191 } 192 } 193 } 194 } 195 // Save 196 context.saveMeasure(resource, CoreMetrics.DUPLICATED_FILES, 1.0); 197 context.saveMeasure(resource, CoreMetrics.DUPLICATED_LINES, (double) duplicatedLines.size()); 198 context.saveMeasure(resource, CoreMetrics.DUPLICATED_BLOCKS, duplicatedBlocks); 199 200 Measure data = new Measure(CoreMetrics.DUPLICATIONS_DATA, toXml(duplications)) 201 .setPersistenceMode(PersistenceMode.DATABASE); 202 context.saveMeasure(resource, data); 203 } 204 205 private static String toXml(Iterable<CloneGroup> duplications) { 206 StringBuilder xml = new StringBuilder(); 207 xml.append("<duplications>"); 208 for (CloneGroup duplication : duplications) { 209 xml.append("<g>"); 210 for (ClonePart part : duplication.getCloneParts()) { 211 xml.append("<b s=\"").append(part.getStartLine()) 212 .append("\" l=\"").append(part.getLines()) 213 .append("\" r=\"").append(part.getResourceId()) 214 .append("\"/>"); 215 } 216 xml.append("</g>"); 217 } 218 xml.append("</duplications>"); 219 return xml.toString(); 220 } 221 222 }