// ===================================================================== // // Copyright (C) 2012 - 2016, Philip Graf // // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // which accompanies this distribution, and is available at // http://www.eclipse.org/legal/epl-v10.html // // ===================================================================== package ch.acanda.eclipse.pmd.cache; import static ch.acanda.eclipse.pmd.domain.ProjectModel.RULESETS_PROPERTY; import static ch.acanda.eclipse.pmd.domain.WorkspaceModel.PROJECTS_PROPERTY; import static java.util.concurrent.TimeUnit.HOURS; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import net.sourceforge.pmd.RuleSets; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import ch.acanda.eclipse.pmd.PMDPlugin; import ch.acanda.eclipse.pmd.builder.LocationResolver; import ch.acanda.eclipse.pmd.domain.DomainModel.AddElementPropertyChangeEvent; import ch.acanda.eclipse.pmd.domain.DomainModel.RemoveElementPropertyChangeEvent; import ch.acanda.eclipse.pmd.domain.LocationContext; import ch.acanda.eclipse.pmd.domain.ProjectModel; import ch.acanda.eclipse.pmd.domain.RuleSetModel; import ch.acanda.eclipse.pmd.domain.WorkspaceModel; import ch.acanda.eclipse.pmd.file.FileChangedListener; import ch.acanda.eclipse.pmd.file.FileWatcher; import ch.acanda.eclipse.pmd.file.Subscription; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; /** * The rule set cache caches the PMD rule sets so they do not have to be rebuilt every time PMD is invoked. * * @author Philip Graf */ public final class RuleSetsCache { /** * Maps a project name to the project's rule sets. */ private final LoadingCache<String, RuleSets> cache; private final ProjectModelListener projectModelListener = new ProjectModelListener(); private final Optional<FileWatcher> fileWatcher; private final Multimap<String, Subscription> subscriptions = HashMultimap.create(); public RuleSetsCache(final CacheLoader<String, RuleSets> loader, final WorkspaceModel workspaceModel) { // by expiring the rule sets we make sure to notice changes in remote configurations cache = CacheBuilder.newBuilder().expireAfterWrite(1, HOURS).build(loader); fileWatcher = createFileWatcher(); for (final ProjectModel projectModel : workspaceModel.getProjects()) { projectModel.addPropertyChangeListener(/* RULESETS_PROPERTY, */projectModelListener); startWatchingRuleSetFiles(projectModel); } workspaceModel.addPropertyChangeListener(PROJECTS_PROPERTY, new WorkspaceModelListener()); } private void startWatchingRuleSetFiles(final ProjectModel projectModel) { if (fileWatcher.isPresent() && projectModel.isPMDEnabled()) { final FileChangedListener listener = new RuleSetFileListener(projectModel); final IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectModel.getProjectName()); for (final RuleSetModel ruleSetModel : projectModel.getRuleSets()) { if (ruleSetModel.getLocation().getContext() != LocationContext.REMOTE) { final Optional<String> resolvedLocation = LocationResolver.resolveIfExists(ruleSetModel.getLocation(), project); if (resolvedLocation.isPresent()) { final Path file = Paths.get(resolvedLocation.get()); try { final Subscription subscription = fileWatcher.get().subscribe(file, listener); subscriptions.put(projectModel.getProjectName(), subscription); } catch (final IOException e) { final String msg = "Cannot watch rule set file %s. " + "Changes to this file will not be picked up for up to an hour."; PMDPlugin.getDefault().warn(String.format(msg, file.toAbsolutePath()), e); } } } } } } private void stopWatchingRuleSetFiles(final ProjectModel projectModel) { for (final Subscription subscription : subscriptions.removeAll(projectModel.getProjectName())) { subscription.cancel(); } } private void resetFileWatcher(final ProjectModel projectModel) { stopWatchingRuleSetFiles(projectModel); startWatchingRuleSetFiles(projectModel); } private Optional<FileWatcher> createFileWatcher() { Optional<FileWatcher> fileWatcher; try { fileWatcher = Optional.of(new FileWatcher()); } catch (final IOException e) { fileWatcher = Optional.absent(); } return fileWatcher; } /** * Returns the PMD rule sets of the provided project. The rule sets are taken from the cache if already available or * loaded from the repository if not. * * @param projectName The name of the project. * @return The PMD rule sets of the project. */ public RuleSets getRuleSets(final String projectName) { return cache.getUnchecked(projectName); } /** * Invalidates the cache entry for the project with the provided name, i.e. the next time * {@link #getRuleSets(String)} is called, the rule sets are loaded from their source. * * @param projectName The name of the project. */ private void invalidate(final String projectName) { PMDPlugin.getDefault().info("Invalidating cache for " + projectName); cache.invalidate(projectName); } /** * Keeps track of added and removed project models. */ private final class WorkspaceModelListener implements PropertyChangeListener { @Override public void propertyChange(final PropertyChangeEvent event) { if (event instanceof AddElementPropertyChangeEvent) { // A project has been added. Add a listener to invalidate its cache entry when its rule sets change final ProjectModel projectModel = (ProjectModel) ((AddElementPropertyChangeEvent) event).getAddedElement(); projectModel.addPropertyChangeListener(/* RULESETS_PROPERTY, */projectModelListener); startWatchingRuleSetFiles(projectModel); } else if (event instanceof RemoveElementPropertyChangeEvent) { // A project has been removed. Invalidate it's cache entry to release the cached resources. final ProjectModel projectModel = (ProjectModel) ((RemoveElementPropertyChangeEvent) event).getRemovedElement(); invalidate(projectModel.getProjectName()); projectModel.removePropertyChangeListener(RULESETS_PROPERTY, projectModelListener); stopWatchingRuleSetFiles(projectModel); } } } /** * Invalidates the cache entry of a project if there have been made any changes to the respective project model so * its rule set is rebuilt based on the new model data the next time it is used. */ private final class ProjectModelListener implements PropertyChangeListener { @Override public void propertyChange(final PropertyChangeEvent event) { final ProjectModel projectModel = (ProjectModel) event.getSource(); invalidate(projectModel.getProjectName()); resetFileWatcher(projectModel); } } /** * Invalidates the respective cache entry when a rule set file is changed. */ private final class RuleSetFileListener implements FileChangedListener { private final String projectName; public RuleSetFileListener(final ProjectModel projectModel) { projectName = projectModel.getProjectName(); } @Override public void fileChanged(final Path file) { invalidate(projectName); } } }