Package net.sf.collabreview.agents

Source Code of net.sf.collabreview.agents.CheckstyleAgent$AuditEventList

/*
   Copyright 2012 Christian Prause and Fraunhofer FIT

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package net.sf.collabreview.agents;

import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
import com.puppycrawl.tools.checkstyle.PropertiesExpander;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import net.sf.collabreview.core.Artifact;
import net.sf.collabreview.core.ArtifactIdentifier;
import net.sf.collabreview.core.Repository;
import net.sf.collabreview.core.configuration.ConfigurationData;
import net.sf.collabreview.core.filter.Filter;
import net.sf.collabreview.core.toolbox.LineCounter;
import net.sf.collabreview.core.users.Author;
import net.sf.collabreview.core.users.AuthorManager;
import net.sf.collabreview.hooks.PostAddHook;
import net.sf.collabreview.repository.Review;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.tree.DefaultDocumentType;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

/**
* Performs Checkstyle analysis on Artifacts and writes a review depending on the results.
* From the Checkstyle web page:
* "Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard.
* It automates the process of checking Java code to spare humans of this boring (but important) task.
* This makes it ideal for projects that want to enforce a coding standard.
* Checkstyle is highly configurable and can be made to support almost any coding standard.
* [...]
* As well, other sample configuration files are supplied for other well known conventions.
* [...]
* Checkstyle provides checks that find class design problems, duplicate code, or bug patterns like double checked locking."
* <p/>
* The CheckstyleAgent will perform an initial check of all artifacts after it has been created.
* (Initial re-checking of all artifacts is controlled with the <code>&lt;recheckAll/&gt;</code> configuration option.)
* At that time it will also add a postadd hook to the repository so that it gets informed when new artifacts are
* added to the repository.
* New artifacts are added to the internal to-do list.
* <p/>
* The CheckstyleAgent will then check the to-do list every 30 seconds.
* If there are entries in the list it will check the styles of these.
* <p/>
* The CheckstyleAgent will only consider artifacts that match its filter option.
* <p/>
* The agent ignores branch information when starting a review.
* This can result in a name clash (see description of method createArtifactFiles())
*
* @author Christian Prause (chris)
* @date 11.04.2009 11:39:40
* @see net.sf.collabreview.agents.CheckstyleAgent#filter
* @see net.sf.collabreview.agents.CheckstyleAgent#createArtifactFiles(java.io.File, java.util.List)
*/
public class CheckstyleAgent implements Agent, PostAddHook, AuditListener {
  private static final Log logger = LogFactory.getLog(CheckstyleAgent.class);

  private final static Map<SeverityLevel, String> SEVERITY_STRING = new HashMap<SeverityLevel, String>();

  static {
    SEVERITY_STRING.put(SeverityLevel.ERROR, "show-stopper");
    SEVERITY_STRING.put(SeverityLevel.WARNING, "problem");
    SEVERITY_STRING.put(SeverityLevel.INFO, "warning");
    SEVERITY_STRING.put(SeverityLevel.IGNORE, "info");
  }

  /**
   * Maximum number of Artifacts to handle in one run.
   *
   * @see CheckstyleAgent#loadTodoArtifacts()
   */
  private static final int MAX_ARTIFACTS_AT_ONCE = 250;

  /**
   * The AgentManager that this agent belongs to.
   */
  private AgentManager agentManager;

  /**
   * Should this agent terminate?
   *
   * @see net.sf.collabreview.agents.CheckstyleAgent#terminate()
   */
  private boolean terminate = false;

  /**
   * When this flag is set to true, then any artifact that gets pushed onto the todoList will be checked.
   * This happens even if there is already an up-to-date review for that artifact.
   * The most notable effect of this flag is that all files are rechecked upon startup as during startup
   * all artifacts are pushed to the list.
   *
   * @see net.sf.collabreview.agents.CheckstyleAgent#loadTodoArtifacts()
   */
  private boolean recheckAll = false;

  /**
   * How many lines does it take until a warning-level problem is statistically cured?
   *
   * @see net.sf.collabreview.agents.CheckstyleAgent#writeReview(net.sf.collabreview.agents.CheckstyleAgent.AuditEventList)
   */
  private int errorCureSize = 50;

  /**
   * The list of artifacts that have been reported changed and have not yet been audited.
   */
  private List<ArtifactIdentifier> todoList = new ArrayList<ArtifactIdentifier>();

  /**
   * When a new run of checkstyle is prepared the to be reviewed files are written to a temporary directory
   * on the disk.
   * These files/directories need to be deleted after the check is completed, and this is the list of files to
   * delete.
   *
   * @see CheckstyleAgent#cleanTemporaryFiles()
   */
  private Stack<File> deleteList = new Stack<File>();

  /**
   * Maps the temporary file on disk to its artifact.
   * The keys can be weak references because the File objects are maintained by the deleteList.
   */
  private Map<File, ArtifactIdentifier> artifactIdentifierForTemporaryFile = new WeakHashMap<File, ArtifactIdentifier>();

  /**
   * The Checkstyle checker that does the audit of the source files.
   */
  private Checker checker;

  /**
   * For each artifact that is being checked by the checker we record the audit events in a AuditEventList.
   * These events are then compiled into the final audit report.
   * The AuditEventList does this by calling its write method.
   * <p/>
   * Afterwards the list is cleared by checkStyles().
   *
   * @see CheckstyleAgent#checkStyles()
   */
  private Map<ArtifactIdentifier, AuditEventList> artifactsAuditEvents = new HashMap<ArtifactIdentifier, AuditEventList>();

  /**
   * The Checkstyle Agent will only process artifacts with an Identifier that matches this filter rule.
   */
  private Filter filter;

  /**
   * How long the agent should sleep between two review cycles.
   */
  private long sleepInterval = 300 * 1000l;

  @Override
  public int getPriority() {
    return PRIORITY_LOW;
  }

  public void configure(AgentManager agentManager, ConfigurationData config) throws Exception {
    assert this.agentManager == null;
    this.agentManager = agentManager;
    configureCheckstyle(config);
    ConfigurationData sleepConfig = config.getSubElement("sleep");
    if (sleepConfig != null) {
      sleepInterval = Long.parseLong(sleepConfig.getSelfValue()) * 1000;
    }
    logger.debug("Agent sleep interval is: " + sleepInterval / 1000 + " seconds");
    // ensure that the checkstyle author exists
    AuthorManager manager = agentManager.getCollabReview().getAuthorManager();
    Author author = manager.getAuthor(getProposedName());
    if (author == null) {
      logger.info("Registering a new author for the CheckstyleAgent: " + getProposedName());
      author = manager.createAuthor(getProposedName());
      if (manager.storeAuthor(author) == null) {
        logger.error("Failed to register the new author");
      } else {
        manager.commit();
      }
    }
    // input filter
    ConfigurationData filterConfig = config.getSubElement("filter");
    if (filterConfig != null) {
      filter = Filter.readFilter(filterConfig);
    }
    // initialize recheck all flag
    ConfigurationData recheckElement = config.getSubElement("recheck");
    if (recheckElement != null) {
      recheckAll = recheckElement.getSelfValue().toLowerCase().equals("true");
    }
    // initialize curesize
    ConfigurationData curesizeElement = config.getSubElement("curesize");
    if (curesizeElement != null) {
      errorCureSize = Integer.parseInt(curesizeElement.getSelfValue());
    }
    // register the hook first so we won't miss any artifact update...
    registerHooks();
    // do the initial scan
    initialScan();
  }

  /**
   * Configures the checkstyle library using the module sub-element in the agent's configuration.
   *
   * @param config the XML element with the agent's configuration which contains the checkstyle configuration as a sub-element
   * @throws CheckstyleException if there is a problem when trying to configure checkstyle
   * @throws DocumentException   the sub-element with the checkstyle configuration cannot be converted into a new XML document
   */
  private void configureCheckstyle(ConfigurationData config) throws CheckstyleException, DocumentException {
    Document document = DocumentHelper.parseText(config.getSubElement("module").toString());
    document.setDocType(new DefaultDocumentType("module", "-//Puppy Crawl//DTD Check Configuration 1.2//EN", "http://www.puppycrawl.com/dtds/configuration_1_2.dtd"));
    //logger.debug("Configuring checkstyle: " + document.asXML());
    com.puppycrawl.tools.checkstyle.api.Configuration csc = ConfigurationLoader.loadConfiguration(
        new ByteArrayInputStream(document.asXML().getBytes()),
        new PropertiesExpander(new Properties()), true);
    checker = new Checker();
    // must be done before calling configure method on checker, otherwise locale setting will not be used
    checker.setLocaleCountry("en");
    checker.setLocaleLanguage("en");
    checker.setModuleClassLoader(CheckstyleAgent.class.getClassLoader());
    checker.addListener(this);
    checker.configure(csc);
  }

  /**
   * Additionally make sure that all temporary files that were created are deleted.
   *
   * @throws Throwable if deleting or super.finalize() fails
   */
  @Override
  protected void finalize() throws Throwable {
    cleanTemporaryFiles();
    super.finalize();
  }

  /**
   * Register hook with the places that this agent wants to listen to
   */
  private void registerHooks() {
    agentManager.getCollabReview().getRepository().registerPostAddHook(this);
  }

  /**
   * When the agent is newly created it will first scan for all non-obsolete files and audit them.
   * <p/>
   * This method is synchronized because it accesses the to-do list which might be used or modified by another thread
   * at the same time.
   * <p/>
   * I do the initial scan in the constructor because this ensures that the run method will immediately
   * find something in the to-do list when it starts. It will not sleep first.
   */
  private synchronized void initialScan() {
    Collection<ArtifactIdentifier> nonObsolete = agentManager.getCollabReview().getRepository().listNonObsoleteArtifacts(null);
    for (ArtifactIdentifier id : nonObsolete) {
      addArtifactToTodoList(id);
    }
    logger.info("Initial scan completed with " + todoList.size() + " artifacts in todo list");
  }

  /**
   * Cleans up the temporary files after the checkstyle run has finished.
   */
  private synchronized void cleanTemporaryFiles() {
    logger.debug("Cleaning " + deleteList.size() + " temporary files");
    while (!deleteList.empty()) {
      File top = deleteList.pop();
      if (!top.delete()) {
        logger.warn("Failed to delete temporary file: " + top.getAbsolutePath());
      }
    }
    artifactIdentifierForTemporaryFile.clear();
  }

  public String getProposedName() {
    return "Checkstyle";
  }

  public void terminate() {
    terminate = true;
  }

  /**
   * Loop until the terminate flag gets set. Then terminate and clean up the temporary files.
   * <p/>
   * Check if there is something in the to-do list.
   * If yes then check the styles of the files in the to-do list.
   * If no then sleep for some time and then repeat the to-do list check...
   */
  public void run() {
    while (!terminate) {
      if (todoList.size() == 0) {
        try {
          Thread.sleep(sleepInterval);
        } catch (InterruptedException e) {
        }
      } else {
        logger.debug("Checking styles of " + todoList.size() + " artifacts");
        try {
          checkStyles();
        } catch (Throwable t) {
          logger.error("CheckstyleAgent worker encountered an uncaught problem. Clearing todoList", t);
          todoList.clear();
        }
      }
    }
    cleanTemporaryFiles();
  }

  /**
   * Prepares and executes the Checkstyle run.
   * Does the actual work.
   * <p/>
   * First loads the to do list and removes duplicates.
   * Next files are created for the artifacts and then checkstyle library is invoked on the files.
   * The collected results are then added as reviews to the artifacts.
   * Finally this method cleans up (removes) all files that were created in preparation of the checkstyle run.
   *
   * @throws Exception if there is a problem while loading the Artifacts
   */
  private void checkStyles() throws Exception {
    // method needs not be synchronized because loadTodoArtifacts() is synchronized and will make a copy of the to-do list
    File workDir = createWorkDir();
    List<Artifact> artifacts = loadTodoArtifacts();
    createArtifactFiles(workDir, artifacts);
    // run checkstyle
    checker.process(new ArrayList<File>(artifactIdentifierForTemporaryFile.keySet()));
    // commit reviews
    for (ArtifactIdentifier ai : artifactsAuditEvents.keySet()) {
      writeReview(artifactsAuditEvents.get(ai));
    }
    artifactsAuditEvents.clear();
    cleanTemporaryFiles();
    agentManager.getCollabReview().getRepository().commit();
  }

  /**
   * Creates the source files for all the artifacts by invoking the creator method for each artifact.
   * <p/>
   * If a name clash is detected because two artifacts would be in the same temporary file then one of the artifacts
   * is pushed back into the to-do list.
   *
   * @param workDir   the working directory where temporary files for Checkstyle should be put
   * @param artifacts the list of artifacts that should be audited
   * @see net.sf.collabreview.agents.CheckstyleAgent#createArtifactFile(java.io.File, net.sf.collabreview.core.Artifact)
   */
  private void createArtifactFiles(File workDir, List<Artifact> artifacts) {
    for (Artifact artifact : artifacts) {
      if (!filter.filter(artifact)) {
        continue;
      }
      File file = createArtifactFile(workDir, artifact);
      if (artifactIdentifierForTemporaryFile.containsKey(file)) {
        logger.warn("Temporary file name collision for " + artifact + " with " + artifactIdentifierForTemporaryFile.get(file) + ". Pushing old artifact back into todo list.");
        this.addArtifactToTodoList(artifactIdentifierForTemporaryFile.get(file));
      }
      artifactIdentifierForTemporaryFile.put(file, artifact.getId());
    }
  }

  /**
   * Creates the temporary file for an artifact.
   * If the name of the artifact contains path separator characters ('/' or '\') then file name parts
   * are considered paths. Required directories are automatically created.
   *
   * @param workDir  the working directory where temporary files for Checkstyle should be put
   * @param artifact the artifact for which a file should be created
   * @return the source file with the artifact's content or null if something failed
   */
  private File createArtifactFile(File workDir, Artifact artifact) {
    String name = artifact.getId().getName();
    String[] nameParts = name.split("/");
    // create required directories
    File target = workDir;
    for (int i = 0; i < nameParts.length - 1; i++) {
      if (nameParts[i].equals("")) {
        continue;
      }
      target = new File(target, nameParts[i]);
      if (!target.exists()) {
        if (!target.mkdir()) {
          throw new NullPointerException("Failed to create required work directory: " + target.getAbsolutePath());
        }
        deleteList.add(target);
      }
    }
    // create file
    target = new File(target, nameParts[nameParts.length - 1]);
    try {
      FileOutputStream fos = new FileOutputStream(target);
      fos.write(artifact.getContent().getBytes());
      fos.close();
      // first removing and then adding the target might seem a bit weird at first but has its reason:
      // the File->Artifact mapping uses weak keys, so it is important that the correct File object is hard referenced.
      // Therefore the old target is removed (if it exists) and then the new (correct) one is added.
      deleteList.remove(target);
      deleteList.add(target);
    } catch (IOException e) {
      logger.error("Failed to create artifact file " + target.getAbsolutePath(), e);
    }
    return target;
  }

  /**
   * Convenience method to return the Checkstyle author.
   *
   * @return the Author as which this Agent publishes its reviews
   */
  private Author getCheckstyleAuthor() {
    return agentManager.getCollabReview().getAuthorManager().getAuthor(getProposedName());
  }

  /**
   * The AgentManager that owns this agent.
   *
   * @return the owner of this CheckstyleAgent
   */
  public AgentManager getAgentManager() {
    return agentManager;
  }

  /**
   * Loads the artifacts from the to do list.
   * The to do list is cleared afterwards.
   * <p/>
   * Before the list of artifacts is returned, it is consolidated. This means that
   * <ul>
   * <li>if two artifacts with the same name and branch are in the list then only the one with the higher revision number will remain in the list, and</li>
   * <li>that the artifact is not reviewed if there is already an up-to-date review (except for when recheckAll flag is set)</li>
   * </ul>
   * <p/>
   * There is a practical problem if there are lots and lots of artifacts to check (as I recently noticed when
   * applying to the Hydra sources).
   * The number of checks that are done in one run should be small enough to sensibly fit into a database transaction.
   * The number is therefore limited. Remaining todos are pushed back to the list...
   * Additionally it is a memory consideration to not load all artifacts at once.
   *
   * @return the Artifacts of the consolidated to do list
   * @throws Exception if there is a problem while loading the Artifacts
   * @see net.sf.collabreview.agents.CheckstyleAgent#MAX_ARTIFACTS_AT_ONCE
   * @see net.sf.collabreview.agents.CheckstyleAgent#recheckAll
   */
  private synchronized List<Artifact> loadTodoArtifacts() throws Exception {
    // consolidated identifier list
    Collection<ArtifactIdentifier> consolidatedList = new HashSet<ArtifactIdentifier>();
    // now check each of the files in the todoList to make sure only the most recent version is checked and that a review does not already exist
    for (int i = 0; i < todoList.size(); i++) {
      ArtifactIdentifier ai1 = todoList.get(i);
      boolean isOk = true;
      Review previousReview;
      // really check this file if recheckAll or no previous review or old review refers to a different artifact
      if (!recheckAll
          && (previousReview = agentManager.getCollabReview().getRepository().getReview(ai1.getName(), ai1.getBranch(), getCheckstyleAuthor())) != null
          && previousReview.getArtifactIdentifier().equals(ai1)) {
        isOk = false;
      }
      // compare to all other Artifacts in the todoList but terminate if the Artifact is found to be a duplicate
      for (int j = i + 1; j < todoList.size() && isOk; j++) {
        ArtifactIdentifier ai2 = todoList.get(j);
        if (ai1 == ai2) {
          // don't add ai1 this time, because the Artifact will be added when it is ai2's turn
          isOk = false;
        }
        if (ai1.getName().equals(ai2.getName()) && ai1.getBranch().equals(ai2.getBranch()) && ai1.getRevision() <= ai2.getRevision()) {
          isOk = false;
        }
      }
      if (isOk) {
        consolidatedList.add(ai1);
      }
    }
    if (consolidatedList.size() != todoList.size()) {
      logger.debug("Consolidated list of artifacts to be done to " + consolidatedList.size() + " entries");
      if (consolidatedList.size() < 5) {
        for (ArtifactIdentifier id : consolidatedList) {
          logger.debug(" Artifact " + id);
        }
      }
    }
    // clear old todolist
    todoList.clear();
    // do not check more than MAX_ARTIFACTS_AT_ONCE artifacts at once
    if (consolidatedList.size() > MAX_ARTIFACTS_AT_ONCE) {
      logger.warn("Too many artifacts in todo list " + consolidatedList.size() + " only working on " + MAX_ARTIFACTS_AT_ONCE + " now, other will be handled later");
      Collection<ArtifactIdentifier> pushback = new HashSet<ArtifactIdentifier>();
      int i = 0;
      for (ArtifactIdentifier id : consolidatedList) {
        if (i > MAX_ARTIFACTS_AT_ONCE) {
          pushback.add(id);
        }
        i++;
      }
      consolidatedList.removeAll(pushback);
      todoList.addAll(pushback);
    }
    // load artifacts
    return agentManager.getCollabReview().getRepository().fetchAll(consolidatedList);
  }

  /**
   * Creates the working directory where temporary files for checkstyle are created.
   * This method is called when a new checkstyle run is being prepared.
   *
   * @return a directory where temporary files for the next checkstyle run should be put in
   * @see CheckstyleAgent#checkStyles()
   */
  private synchronized File createWorkDir() {
    File root = new File(System.getProperty("java.io.tmpdir"), "CollabReviewCheckstyle");
    if (root.exists()) {
      logger.warn("The working directory already exists: " + root.getAbsolutePath());
    } else {
      if (!root.mkdir()) {
        logger.error("Failed to create temporary working directory: " + root.getAbsolutePath());
        return null;
      } else {
        logger.debug("Created checkstyle working directory: " + root.getAbsolutePath());
        deleteList.add(root);
      }
    }
    return root;
  }

  /**
   * Handle repository's artifactAdded() event by adding the artifact to the to-do list.
   *
   * @param repository ignored
   * @param artifact   its identifier is added to the to-do list
   */
  public void artifactAdded(Repository repository, Artifact artifact) {
    logger.debug("Adding artifact to todo list: " + artifact.getId());
    addArtifactToTodoList(artifact.getId());
  }

  /**
   * Add an artifact to the to-do list if it matches the filter rule
   *
   * @param id identifier of the artifact to add
   * @see net.sf.collabreview.agents.CheckstyleAgent#filter
   */
  private synchronized void addArtifactToTodoList(ArtifactIdentifier id) {
    if (filter.preFilter(id.getName(), id.getRevision(), id.getBranch())) {
      todoList.add(id);
    }
  }

  /**
   * Does nothing (but issue a log message) with this audit event from Checkstyle
   *
   * @param auditEvent the event
   */
  public void auditStarted(AuditEvent auditEvent) {
    logger.debug("Audit started");
  }

  /**
   * Does nothing (but issue a log message) with this audit event from Checkstyle
   *
   * @param auditEvent the event
   */
  public void auditFinished(AuditEvent auditEvent) {
    logger.debug("Audit finished");
  }

  /**
   * Just load the AuditEventList for this audit event's file to make sure that it exists and thereby
   * the report gets written even if there are no problems with this artifact.
   *
   * @param auditEvent the event
   */
  public void fileStarted(AuditEvent auditEvent) {
    getAuditEventListForAuditEvent(auditEvent);
  }

  /**
   * Does nothing with this audit event from Checkstyle
   *
   * @param auditEvent the event
   */
  public void fileFinished(AuditEvent auditEvent) {
  }

  /**
   * Gets the AuditEventList to which an auditEvent must be added.
   *
   * @param auditEvent the audit event that is asking for its list
   * @return the AuditEventList for audit events about the same artifact or a new list if this is the first audit event about the artifact
   */
  private AuditEventList getAuditEventListForAuditEvent(AuditEvent auditEvent) {
    File sourceFile = new File(auditEvent.getFileName());
    ArtifactIdentifier ai = artifactIdentifierForTemporaryFile.get(sourceFile);
    if (ai == null) {
      logger.warn("No source file: " + sourceFile);
      return null;
    } else {
      AuditEventList list = artifactsAuditEvents.get(ai);
      if (list == null) {
        list = new AuditEventList(ai);
        artifactsAuditEvents.put(ai, list);
      }
      return list;
    }
  }

  public void addError(AuditEvent auditEvent) {
    AuditEventList list = getAuditEventListForAuditEvent(auditEvent);
    if (list != null) {
      list.add(auditEvent);
    }
  }

  public void addException(AuditEvent auditEvent, Throwable throwable) {
    AuditEventList list = getAuditEventListForAuditEvent(auditEvent);
    if (list != null) {
      list.hasException = true;
    }
    logger.warn("Exception occurred when auditing " + auditEvent.getFileName(), throwable);
  }

  /**
   * Returns the line string from the source that is specified by the AuditEvent.
   *
   * @param auditEvent the event for which the source code is to be obtained
   * @return the source code to which this audit event refers
   */
  private String getEventSourceLine(AuditEvent auditEvent) {
    File sourceFile = new File(auditEvent.getFileName());
    ArtifactIdentifier ai = artifactIdentifierForTemporaryFile.get(sourceFile);
    if (ai == null) {
      logger.warn("No source file: " + sourceFile);
      return null;
    }
    String source = agentManager.getCollabReview().getRepository().getArtifact(ai).getContent();
    int line = auditEvent.getLine();
    if (line == 0) {
      logger.warn("No line because no relation to source file " + auditEvent.getFileName());
      return null;
    }
    // checkstyle starts counting at line 1, not 0
    line--;
    String[] lines = source.split("\n");
    if (lines.length <= line) {
      logger.warn("Line " + line + " does not exist (somehow?!)");
      return null;
    }
    return lines[line];
  }

  /**
   * A list of AuditEvents for one artifact.
   * <p/>
   * Whenever a new AuditEvent is added to the list some evaluations (like counting the number of events
   * from different severities) are already made.
   */
  protected class AuditEventList {
    /**
     * The complete list of all AuditEvents that occurred.
     */
    private List<AuditEvent> events = new ArrayList<AuditEvent>();

    /**
     * The number of info AuditEvents
     *
     * @see net.sf.collabreview.agents.CheckstyleAgent.AuditEventList#increaseInfoCount();
     */
    private int infoCount = 0;

    /**
     * The number of warning severity level AuditEvents
     *
     * @see net.sf.collabreview.agents.CheckstyleAgent.AuditEventList#increaseWarnCount();
     */
    private int warnCount = 0;

    /**
     * The number of error severity level AuditEvents
     *
     * @see net.sf.collabreview.agents.CheckstyleAgent.AuditEventList#increaseErrorCount();
     */
    private int errorCount = 0;

    /**
     * Has an exception occurred when this list's artifact was reviewed?
     */
    private boolean hasException = false;

    /**
     * References the artifact to which this list belongs.
     */
    private ArtifactIdentifier artifactIdentifier;

    /**
     * Creates an emptry AuditEventList.
     * A AuditEventList is always associated with a specific Artifact.
     *
     * @param artifactIdentifier to which Artifact this AuditEventList belongs.
     */
    public AuditEventList(ArtifactIdentifier artifactIdentifier) {
      this.artifactIdentifier = artifactIdentifier;
    }

    /**
     * Add a new AuditEvent to this list and do some precomputations.
     *
     * @param ae the new AuditEvent to add
     */
    public void add(AuditEvent ae) {
      events.add(ae);
      if (ae.getSeverityLevel() == SeverityLevel.INFO
          // if line == 0 then there is something wrong which is not directly related to the line. Just report it...
          // A possible reason is a "throws" statement with an Exception that Checkstyle does not find.
          || ae.getLine() == 0) {
        increaseInfoCount();
      } else if (ae.getSeverityLevel() == SeverityLevel.WARNING) {
        increaseWarnCount();
      } else if (ae.getSeverityLevel() == SeverityLevel.ERROR) {
        increaseErrorCount();
      }
      assert getInfoCount() + getWarnCount() + getErrorCount() == events.size();
    }

    /**
     * Internal method that increments the info event counter.
     * But only to the limit value any info events after that one are carried over the next higher event category.
     *
     * @see net.sf.collabreview.agents.CheckstyleAgent.AuditEventList#infoCount
     */
    private void increaseInfoCount() {
      infoCount++;
    }

    /**
     * Internal method that increments the warning event counter.
     *
     * @see net.sf.collabreview.agents.CheckstyleAgent.AuditEventList#warnCount
     */
    private void increaseWarnCount() {
      warnCount++;
    }

    /**
     * Internal method that increments the error event counter.
     *
     * @see net.sf.collabreview.agents.CheckstyleAgent.AuditEventList#errorCount
     */
    private void increaseErrorCount() {
      errorCount++;
    }

    /**
     * Get the number of info level violations.
     *
     * @return the number of violations of level INFO
     */
    public int getInfoCount() {
      return infoCount;
    }

    /**
     * Get the number of warning level violations.
     *
     * @return the number of violations of level WARN
     */
    public int getWarnCount() {
      return warnCount;
    }

    /**
     * Get the number of error level violations.
     *
     * @return the number of violations of level ERROR
     */
    public int getErrorCount() {
      return errorCount;
    }

    /**
     * Get the full list of AuditEvents that occurred.
     *
     * @return the complete list of events for the artifact
     */
    public List<AuditEvent> getEvents() {
      return events;
    }

    /**
     * Did an exception occur while Checkstyle was auditing the artifact?
     *
     * @return true iff Checkstyle reported an exception while auditing
     */
    public boolean hasException() {
      return hasException;
    }

    /**
     * Get a ArtifactIdentifier of the Artifact that this AuditEventList belongs to.
     *
     * @return ArtifactIdentifier of the Artifact to which this AuditEventList belongs.
     */
    public ArtifactIdentifier getArtifactIdentifier() {
      return artifactIdentifier;
    }
  }

  /**
   * Uses the information from the specified AuditEventList to write a review for the artifact that is referenced in the AuditEventList.
   * The review is automatically submitted to the Repository.
   * <p/>
   * The rating is computed according to the following rules:
   * <ul>
   * <li>if there is at least one error-level problem the rating is -10 regardless of the other rules</li>
   * <li>the default rating is +7</li>
   * <li>a warning-level problem and (about) every third info-level problem cost one rating point (acutally not exactly because the function is non-linear f(x)=x^(3/5) )</li>
   * <li>every "errorCureSize" lines cures one warning level problem (or 3 info level problems). This can even improve the score above default level. So, an artifact with errorCureSize * 3 lines without errors has rating +10</li>
   * <li>the rating cannot be better than +10 or worse than -10</li>
   * </ul>
   *
   * @param auditEventList the list of AuditEvents
   * @see net.sf.collabreview.agents.CheckstyleAgent#errorCureSize
   */
  public void writeReview(AuditEventList auditEventList) {
    int size = LineCounter.countNonEmptyLines(agentManager.getCollabReview().getRepository(), auditEventList.getArtifactIdentifier());
    double totalNonComplianceValue = auditEventList.getInfoCount() * 0.3 + auditEventList.getWarnCount();
    // consider the cure size which cures one "problem" (warning) every x lines
    totalNonComplianceValue -= size / errorCureSize;
    // assume a non-linear non-compliance function to reduce the number of -10s a bit
    if (totalNonComplianceValue > 0) {
      totalNonComplianceValue = Math.pow(totalNonComplianceValue, 3f / 5);
    }
    // determine rating and cut to [-10,+10] interval
    int rating = Math.min(10, Math.max(-10, (int) Math.round(7 - totalNonComplianceValue)));
    // if there is at least one error then the artifact has -10 quality irregardless of its number of lines
    if (auditEventList.getErrorCount() > 0) {
      rating = -10;
    }
    StringBuilder sb = new StringBuilder();
    if (auditEventList.getInfoCount() + auditEventList.getWarnCount() + auditEventList.getErrorCount() == 0) {
      sb.append("I see no problem with this file.");
    } else {
      sb.append(String.format("I have some problems with this file (%d %s(s), %d %s(s), %d %s(s)). The most critical ones are: ",
          auditEventList.getErrorCount(), SEVERITY_STRING.get(SeverityLevel.ERROR),
          auditEventList.getWarnCount(), SEVERITY_STRING.get(SeverityLevel.WARNING),
          auditEventList.getErrorCount(), SEVERITY_STRING.get(SeverityLevel.INFO)));
      Collections.sort(auditEventList.getEvents(), new Comparator<AuditEvent>() {
        public int compare(AuditEvent o1, AuditEvent o2) {
          return -o1.getSeverityLevel().compareTo(o2.getSeverityLevel());
        }
      });
      for (int i = 0; i < auditEventList.getEvents().size() && i < 7; i++) {
        AuditEvent ae = auditEventList.getEvents().get(i);
        sb.append(String.format("\n%d. (line %d, criticality: %s): %s Source: %s", i + 1, ae.getLine(), SEVERITY_STRING.get(ae.getSeverityLevel()), ae.getMessage(), getEventSourceLine(ae)));
      }
    }
    if (auditEventList.hasException()) {
      sb.append("\nAn exception has occurred when this artifact was audited. There might be more problems that are not listed here.");
    } else {
      if (rating == 10) {
        sb.append("\nPerfect!");
      } else if (rating == 9) {
        sb.append("\nWell done!");
      } else if (rating == -10) {
        sb.append("WAAAAHH, PANIC! Awful code quality... :-(");
      }
    }
    logger.debug("Adding review with rating " + rating + " to " + auditEventList.getArtifactIdentifier());
    agentManager.getCollabReview().getRepository().setReview(auditEventList.getArtifactIdentifier(), agentManager.getCollabReview().getAuthorManager().getAuthor(getProposedName()), rating, sb.toString(), false);
  }
}
TOP

Related Classes of net.sf.collabreview.agents.CheckstyleAgent$AuditEventList

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.