org.cloudsmith.geppetto.validation.impl.ValidationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudsmith.geppetto.validation.impl.ValidationServiceImpl.java

Source

/**
 * Copyright (c) 2011, 2012 Cloudsmith Inc. and other contributors, as listed below.
 * 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
 * 
 * Contributors:
 *   Cloudsmith
 * 
 */
package org.cloudsmith.geppetto.validation.impl;

import static org.cloudsmith.geppetto.forge.Forge.MODULEFILE_NAME;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.cloudsmith.geppetto.common.os.FileUtils;
import org.cloudsmith.geppetto.diagnostic.DetailedFileDiagnostic;
import org.cloudsmith.geppetto.diagnostic.Diagnostic;
import org.cloudsmith.geppetto.diagnostic.DiagnosticType;
import org.cloudsmith.geppetto.diagnostic.ExceptionDiagnostic;
import org.cloudsmith.geppetto.forge.Forge;
import org.cloudsmith.geppetto.forge.v2.model.Dependency;
import org.cloudsmith.geppetto.forge.v2.model.Metadata;
import org.cloudsmith.geppetto.forge.v2.model.ModuleName;
import org.cloudsmith.geppetto.pp.dsl.PPDSLConstants;
import org.cloudsmith.geppetto.pp.dsl.adapters.ResourcePropertiesAdapter;
import org.cloudsmith.geppetto.pp.dsl.adapters.ResourcePropertiesAdapterFactory;
import org.cloudsmith.geppetto.pp.dsl.linking.PPSearchPath;
import org.cloudsmith.geppetto.pp.dsl.target.PuppetTarget;
import org.cloudsmith.geppetto.pp.dsl.validation.DefaultPotentialProblemsAdvisor;
import org.cloudsmith.geppetto.pp.dsl.validation.IPPDiagnostics;
import org.cloudsmith.geppetto.pp.dsl.validation.IPotentialProblemsAdvisor;
import org.cloudsmith.geppetto.pp.dsl.validation.IValidationAdvisor;
import org.cloudsmith.geppetto.pp.dsl.validation.ValidationPreference;
import org.cloudsmith.geppetto.ruby.RubyHelper;
import org.cloudsmith.geppetto.ruby.RubySyntaxException;
import org.cloudsmith.geppetto.ruby.spi.IRubyIssue;
import org.cloudsmith.geppetto.ruby.spi.IRubyParseResult;
import org.cloudsmith.geppetto.semver.Version;
import org.cloudsmith.geppetto.semver.VersionRange;
import org.cloudsmith.geppetto.validation.FileType;
import org.cloudsmith.geppetto.validation.IValidationConstants;
import org.cloudsmith.geppetto.validation.ValidationOptions;
import org.cloudsmith.geppetto.validation.ValidationService;
import org.cloudsmith.geppetto.validation.runner.AllModuleReferences;
import org.cloudsmith.geppetto.validation.runner.BuildResult;
import org.cloudsmith.geppetto.validation.runner.MetadataInfo;
import org.cloudsmith.geppetto.validation.runner.MetadataInfo.Resolution;
import org.cloudsmith.geppetto.validation.runner.PPDiagnosticsRunner;
import org.cloudsmith.geppetto.validation.runner.PuppetCatalogCompilerRunner;
import org.cloudsmith.geppetto.validation.runner.RakefileInfo;
import org.cloudsmith.geppetto.validation.runner.RakefileInfo.Rakefile;
import org.cloudsmith.geppetto.validation.runner.RakefileInfo.Raketask;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.diagnostics.Severity;
import org.eclipse.xtext.util.CancelIndicator;
import org.eclipse.xtext.validation.CheckMode;
import org.eclipse.xtext.validation.IResourceValidator;
import org.eclipse.xtext.validation.Issue;

import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * Note that all use of monitor assumes SubMonitor semantics (the receiver does *not* call done on monitors, this is the
 * responsibility of the caller if caller is not using a SubMonitor).
 */
public class ValidationServiceImpl implements ValidationService {
    /**
     * 
     */
    private static final String NAME_OF_DIR_WITH_RESTRICTED_SCOPE = "roles";

    private static final FileFilter directoryFilter = new FileFilter() {

        @Override
        public boolean accept(File f) {
            return f.isDirectory() && !f.getName().equals(".svn");
        }

    };

    private static final FilenameFilter ppFileFilter = new FilenameFilter() {

        @Override
        public boolean accept(File dir, String name) {
            return name.endsWith(".pp") && new File(dir, name).isFile();
        }

    };

    private static final FilenameFilter rbFileFilter = new FilenameFilter() {

        @Override
        public boolean accept(File dir, String name) {
            return name.endsWith(".rb") && new File(dir, name).isFile();
        }

    };

    private static final FilenameFilter RakefileFileFilter = new FilenameFilter() {

        @Override
        public boolean accept(final File dir, final String name) {
            String lcname = name.toLowerCase();
            if (lcname.startsWith("rakefile")) {
                int length = lcname.length();
                if (!(length == 8 //
                        || (length == 11 && lcname.endsWith(".rb")) //
                        || (length == 13 && lcname.endsWith(".rake"))))
                    return false;
            } else if (!lcname.endsWith(".rake")) {
                return false;
            }
            // If the file is a directory, or something else, do not include it
            return new File(dir, name).isFile();

            // // quickly disqualify those that are not [Rr]akefile[.rb]
            // // (Do case independent comparison since some file systems do not discriminate on case)
            // int namelength = name.length();
            // if(!(namelength == 8 || namelength == 11))
            // return false;
            // switch(name.charAt(0)) {
            // case 'r':
            // case 'R':
            // break;
            // default:
            // return false;
            // }
            // String lcname = name.toLowerCase();
            // if(!"rakefile".equals(lcname.substring(0, 8)))
            // return false;
            // if(namelength == 11 && !lcname.endsWith(".rb"))
            // return false;

        }

    };

    /**
     * Add an exception diagnostic (not associated with any particular file).
     * 
     * @param diagnostics
     * @param message
     * @param e
     */
    private static void addExceptionDiagnostic(Diagnostic diagnostics, String message, Exception e) {
        ExceptionDiagnostic bd = new ExceptionDiagnostic(Diagnostic.ERROR, INTERNAL_ERROR, message, e);
        diagnostics.addChild(bd);
    }

    /**
     * Add diagnostic for a file.
     * 
     * @param diagnostics
     * @param severity
     * @param file
     * @param rootDirectory
     *            - used to relativize the file path in the report
     * @param message
     * @param issueId
     */
    private static void addFileDiagnostic(Diagnostic diagnostics, int severity, File file, File rootDirectory,
            String message, String issueId) {

        DetailedFileDiagnostic dft = new DetailedFileDiagnostic(severity, UNKNOWN, message,
                pathToFile(file.getAbsolutePath(), rootDirectory));
        dft.setLineNumber(-1);
        dft.setLength(-1);
        dft.setOffset(-1);
        dft.setIssue(issueId);
        dft.setIssueData(new String[] {});
        dft.setNode("");
        diagnostics.addChild(dft);
    }

    private static void addFileError(Diagnostic diagnostics, File file, File rootDirectory, String message,
            String issueId) {
        addFileDiagnostic(diagnostics, Diagnostic.ERROR, file, rootDirectory, message, issueId);
    }

    private static void addFileWarning(Diagnostic diagnostics, File file, File rootDirectory, String message,
            String issueId) {
        addFileDiagnostic(diagnostics, Diagnostic.WARNING, file, rootDirectory, message, issueId);
    }

    /**
     * Translate and add Xtext issue diagnostics to the chain.
     * 
     * @param diagnostics
     * @param issue
     * @param rootDirectory
     * @param rootDirectory
     */
    private static void addIssueDiagnostic(Diagnostic diagnostics, Issue issue, File processedFile,
            File rootDirectory) {
        DiagnosticType type = issue.isSyntaxError() ? GEPPETTO_SYNTAX : GEPPETTO;
        DetailedFileDiagnostic dft = new DetailedFileDiagnostic(translateIssueSeverity(issue.getSeverity()), type,
                issue.getMessage(), uriToFile(issue.getUriToProblem(), rootDirectory));
        dft.setLineNumber(issue.getLineNumber());
        dft.setLength(issue.getLength());
        dft.setOffset(issue.getOffset());
        dft.setIssue(issue.getCode());
        dft.setIssueData(issue.getData());
        dft.setNode("");
        diagnostics.addChild(dft);
    }

    /**
     * Translate and add ruby issue diagnostics to the chain.
     * 
     * @param diagnostics
     * @param issue
     * @param rootDirectory
     * @param rootDirectory
     */
    private static void addRubyIssueDiagnostic(Diagnostic diagnostics, IRubyIssue issue, File processedFile,
            File rootDirectory) {

        int severity = issue.isSyntaxError() ? Diagnostic.ERROR : Diagnostic.WARNING;
        DiagnosticType type = issue.isSyntaxError() ? RUBY_SYNTAX : RUBY;

        DetailedFileDiagnostic dft = new DetailedFileDiagnostic(severity, type, issue.getMessage(),
                pathToFile(issue.getFileName(), rootDirectory));
        dft.setLineNumber(issue.getLine());
        dft.setLength(issue.getLength());
        dft.setOffset(issue.getStartOffset());
        dft.setIssue(issue.getIdString());
        dft.setIssueData(new String[] {}); // TODO: the Ruby issue passes // Object[]
        dft.setNode("");
        diagnostics.addChild(dft);
    }

    /**
     * Translate a file path to a file relative to rootFolder (if under this root, else return an absolute File).
     * 
     * @param uri
     * @param rootFolder
     *            - root directory/folder or a file name
     * @return
     */
    private static File pathToFile(String filePath, File rootFolder) {
        Path problemPath = new Path(filePath);
        Path rootPath = new Path(rootFolder.getPath());
        IPath relativePath = problemPath.makeRelativeTo(rootPath);
        return relativePath.toFile();
    }

    /**
     * Translate the issue severity (an enum) to BasicDiagnostic (they are probably the same...)
     * 
     * @param severity
     * @return
     */
    private static int translateIssueSeverity(Severity severity) {
        switch (severity) {
        case INFO:
            return Diagnostic.INFO;
        case WARNING:
            return Diagnostic.WARNING;

        default:
        case ERROR:
            return Diagnostic.ERROR;

        }
    }

    /**
     * Translate a URI to the file to a file relative to rootFolder (if under this root, else return an absolute File).
     * 
     * @param uri
     * @param rootFolder
     *            - root directory/folder or a file name
     * @return
     */
    private static File uriToFile(URI uri, File rootFolder) {
        // relativize file:
        if (!uri.isFile())
            return null;
        Path problemPath = new Path(uri.toFileString());
        Path rootPath = new Path(rootFolder.getPath());
        IPath relativePath = problemPath.makeRelativeTo(rootPath);
        return relativePath.toFile();
    }

    @Inject
    private Forge forge;

    @Inject
    @Named(Forge.MODULE_FILE_FILTER)
    private FileFilter moduleFileFilter;

    /**
     * Asserts a file/directory's existence and that it can be read.
     * 
     * @throw IllegalArgumentException if the file is not the right type, does not exists, or is not readable.
     * @param file
     * @param assertIsDirectory
     *            - check if file is a directory
     */
    private void assertFileAvailability(File file, boolean assertIsDirectory) {
        if (assertIsDirectory) {
            if (file.isDirectory() && file.canRead())
                return;
            throw new IllegalArgumentException("The file: " + file.toString() + " is not a readable directory.");
        } else if (file.exists() && file.canRead())
            return;
        throw new IllegalArgumentException("The file: " + file.toString() + " is not a readable file.");
    }

    /**
     * @param moduleData
     *            - resolved module data
     * @param root
     *            - root file for relativization
     * @param diagnostics
     *            - where to report issues
     */
    private void checkCircularDependencies(Multimap<ModuleName, MetadataInfo> moduleData, Diagnostic diagnostics,
            File root) {
        // problems: multiple versions of the same, etc.Use an identity set
        for (MetadataInfo mi : moduleData.values()) {
            Set<MetadataInfo> checkedModules = Sets.newIdentityHashSet();
            List<MetadataInfo> circle = Lists.newLinkedList();
            checkCircularity(mi, mi, circle, checkedModules);
        }
    }

    /**
     * @param head
     * @param circle
     * @param checkedModules
     */
    private void checkCircularity(MetadataInfo head, MetadataInfo current, List<MetadataInfo> circle,
            Set<MetadataInfo> checkedModules) {
        if (circle.contains(current))
            return;

        circle.add(0, current);
        for (Resolution r : current.getResolvedDependencies()) {
            if (r.metadata == head) {
                // circular
                head.addCircularity(circle);
            } else {
                // non circular
                checkCircularity(head, r.metadata, circle, checkedModules);
            }
        }
        // pop current
        circle.remove(0);
    }

    private void checkModuleLayout(Diagnostic diagnostics, File moduleRoot, File sourceRoot) {
        if (hasModulesSubDirectory(moduleRoot))
            addFileError(diagnostics, new File(moduleRoot, "modules"), sourceRoot,
                    "Submodules in a module is not allowed",
                    IValidationConstants.ISSUE__UNEXPECTED_SUBMODULE_DIRECTORY);
        if (!forge.hasModuleMetadata(moduleRoot, null))
            addFileError(diagnostics, moduleRoot, sourceRoot, "Missing 'metadata.json or Modulefile'",
                    IValidationConstants.ISSUE__MISSING_MODULEFILE);

    }

    private void checkPuppetRootLayout(Diagnostic diagnostics, File moduleRoot, File sourceRoot) {
        // TODO: check each module under modules
        // TODO: additional checks (files that are required etc.)
    }

    /**
     * Collects file matching filter while skipping all symbolically linked files.
     * 
     * @param root
     * @param filter
     * @param result
     */
    private void collectFiles(File root, FilenameFilter filter, List<File> result) {
        for (File f : root.listFiles(moduleFileFilter)) {
            if (FileUtils.isSymlink(f))
                continue;
            if (filter.accept(f.getParentFile(), f.getName()))
                result.add(f);
            if (directoryFilter.accept(f))
                collectFiles(f, filter, result);
        }
    }

    private List<File> findFiles(File root, FilenameFilter filter) {
        List<File> result = Lists.newArrayList();
        collectFiles(root, filter, result);
        return result;

    }

    private List<File> findPPFiles(File root) {
        return findFiles(root, ppFileFilter);
    }

    private List<File> findRakefiles(File root) {
        return findFiles(root, RakefileFileFilter);
    }

    private List<File> findRubyFiles(File root) {
        return findFiles(root, rbFileFilter);
    }

    /**
     * @param rubyHelper
     * @param f
     * @param root
     * @param newChild
     * @throws RubySyntaxException
     * @throws IOException
     */
    private Rakefile getRakefileInformation(RubyHelper rubyHelper, File f, File root, IProgressMonitor monitor) {
        final SubMonitor ticker = SubMonitor.convert(monitor, 1);

        try {
            Map<String, String> taskInfo = rubyHelper.getRakefileTaskDescriptions(f);
            Path rootPath = new Path(root.getAbsolutePath());
            Path rakefilePath = new Path(f.getAbsolutePath());
            Rakefile result = new Rakefile(rakefilePath.makeRelativeTo(rootPath));

            if (taskInfo == null)
                return result;

            for (Entry<String, String> entry : taskInfo.entrySet())
                result.addTask(new Raketask(entry.getKey(), entry.getValue()));
            return result;
        } catch (IOException e) {
            // simply do not return any information - validation should have dealt with errors
            // System.err.println("IOException while processing Rakefile: " + e.getMessage());
            return null;
        } catch (RubySyntaxException e) {
            // simply do not return any information - validation should have dealt with errors
            // System.err.println("RubySyntaxException while processing Rakefile: " + e.getMessage());
            return null;
        } finally {
            worked(ticker, 1);
            // System.err.println("Processed one Rakefile: " + f.getPath());

        }
    }

    private boolean hasModulesSubDirectory(File root) {
        File modulesDir = new File(root, "modules");
        return modulesDir.isDirectory();
    }

    private boolean isOnPath(File f, PPSearchPath searchPath) {

        return -1 != searchPath.searchIndexOf(URI.createFileURI(f.getPath()));
    }

    private boolean isValidationWanted(File[] examinedFiles, File f) {
        if (examinedFiles == null || examinedFiles.length == 0)
            return true;
        try {
            IPath filePath = Path.fromOSString(f.getCanonicalPath());
            for (int i = 0; i < examinedFiles.length; i++) {
                File x = examinedFiles[i];
                IPath xPath = Path.fromOSString(x.getCanonicalPath());
                if (x.isDirectory()) {
                    if (xPath.isPrefixOf(filePath))
                        return true;
                } else {
                    if (xPath.equals(filePath))
                        return true;
                }
            }
        } catch (IOException e) {
            // the file or the entry in examined files does not exists
            // just continue
        }
        return false;
    }

    /**
     * Loads Modulefile and returns the parsed Metadata. If parsing fails an error is reported on the diagnostics chain
     * and null is returned
     * 
     * @param diagnostics
     * @param moduleFile
     * @param parentFile
     * @param monitor
     * @return null if the Modulefile could not be loaded
     */
    private Metadata loadModuleMetadata(Diagnostic diagnostics, File parentFile, File[] mdProvider,
            IProgressMonitor monitor) {
        // parse the metadata file and get full name and version, use this as
        // name of target entry
        try {
            return forge.createFromModuleDirectory(parentFile, false, null, mdProvider, diagnostics);
        } catch (Exception e) {
            addFileError(diagnostics, new File(MODULEFILE_NAME), parentFile,
                    "Can not parse file: " + e.getMessage(), IValidationConstants.ISSUE__MODULEFILE_PARSE_ERROR);
        }
        return null;
    }

    private void rememberRootInResource(File root, Resource r) {
        if (root == null)
            throw new IllegalArgumentException("root can not be null");
        if (r == null)
            throw new IllegalArgumentException("resource can not be null");
        URI uri = URI.createFileURI(root.getAbsolutePath());
        ResourcePropertiesAdapter adapter = ResourcePropertiesAdapterFactory.eINSTANCE.adapt(r);
        adapter.put(PPDSLConstants.RESOURCE_PROPERTY__ROOT_URI, uri);
    }

    /**
     * @param monitor
     *            - client should call done unless using a SubMonitor
     */
    public BuildResult validate(Diagnostic diagnostics, File source, ValidationOptions options,
            File[] examinedFiles, IProgressMonitor monitor) {
        if (diagnostics == null)
            throw new IllegalArgumentException("diagnostics can not be null");
        if (source == null)
            throw new IllegalArgumentException("source can not be null");
        if (!source.exists())
            throw new IllegalArgumentException("source does not exist");
        if (!source.canRead())
            throw new IllegalArgumentException("source can not be read");
        if (options == null) {
            options = new ValidationOptions();
            options.setCheckLayout(false);
            options.setCheckModuleSemantics(false);
            options.setCheckReferences(false);
            options.setFileType(FileType.DETECT);
        }
        String sourceName = source.getName();
        boolean isDirectory = source.isDirectory();
        boolean isPP = !isDirectory && sourceName.endsWith(".pp");
        boolean isRB = !isDirectory && sourceName.endsWith(".rb");
        boolean isModulefile = !isDirectory && forge.isMetadataFile(source.getName());

        if (!isDirectory && examinedFiles != null && examinedFiles.length != 0)
            throw new IllegalArgumentException("examinedFiles must be empty when source is a regular file");

        if (options.getFileType() == FileType.DETECT) {
            if (!isDirectory)
                options.setFileType(FileType.SINGLE_SOURCE_FILE);
            else {
                // A directory that does not have a 'Modulefile' or other
                // recognized module metadata is treated as a root
                // A directory that has a "modules" subdirectory is treated as a
                // root
                if (hasModulesSubDirectory(source) || !forge.hasModuleMetadata(source, null))
                    options.setFileType(FileType.PUPPET_ROOT);
                else
                    options.setFileType(FileType.MODULE_ROOT);
            }
        }
        boolean rubyServicesPresent = false;

        if (options.getFileType() == FileType.SINGLE_SOURCE_FILE) {
            if (isDirectory)
                throw new IllegalArgumentException("source is not a single source file as stated in options");
            if (isPP) {
                PPDiagnosticsRunner ppDr = new PPDiagnosticsRunner();
                try {
                    IValidationAdvisor.ComplianceLevel complianceLevel = options.getComplianceLevel();
                    if (complianceLevel == null)
                        complianceLevel = IValidationAdvisor.ComplianceLevel.PUPPET_2_7;
                    IPotentialProblemsAdvisor potentialProblems = options.getProblemsAdvisor();
                    if (potentialProblems == null)
                        potentialProblems = new DefaultPotentialProblemsAdvisor();
                    ppDr.setUp(complianceLevel, potentialProblems);
                    validatePPFile(ppDr, diagnostics, source, source.getParentFile(), monitor);
                } catch (Exception e) {
                    addExceptionDiagnostic(diagnostics,
                            "Internal error: Exception while setting up/tearing down validation", e);
                } finally {
                    ppDr.tearDown();
                }

            } else if (isRB) {
                RubyHelper rubyHelper = new RubyHelper();
                rubyServicesPresent = rubyHelper.isRubyServicesAvailable();
                try {
                    rubyHelper.setUp();
                    validateRubyFile(rubyHelper, diagnostics, source, source.getParentFile(), monitor);
                } catch (Exception e) {
                    addExceptionDiagnostic(diagnostics,
                            "Internal error: Exception while setting up/tearing down pp validation", e);
                } finally {
                    rubyHelper.tearDown();
                }

            } else if (isModulefile)
                validateModulefile(diagnostics, source.getParentFile(), options, monitor);
            else
                throw new IllegalArgumentException("unsupported source type");
            return new BuildResult(rubyServicesPresent);
        }

        if (!isDirectory)
            throw new IllegalArgumentException("source is not a directory as dictated by options");

        return validateDirectory(diagnostics, source, options, examinedFiles, monitor);
    }

    /**
     * TODO: Is currently limited to .pp content.
     */
    public Resource validate(Diagnostic diagnostics, String code, IProgressMonitor monitor) {
        if (diagnostics == null)
            throw new IllegalArgumentException("DiagnosticChain can not be null");
        if (code == null)
            throw new IllegalArgumentException("code can not be null");
        final SubMonitor ticker = SubMonitor.convert(monitor, 3);

        PPDiagnosticsRunner ppRunner = new PPDiagnosticsRunner();
        Resource r = null;
        worked(ticker, 1);
        try {
            ppRunner.setUp(IValidationAdvisor.ComplianceLevel.PUPPET_2_7, new DefaultPotentialProblemsAdvisor());
            worked(ticker, 1);
            File f = new File("/unnamed.pp");
            r = ppRunner.loadResource(code, URI.createFileURI(f.getPath()));
            // no need to remember "/" as the root
            IResourceValidator rv = ppRunner.getPPResourceValidator();
            final CancelIndicator cancelMonitor = new CancelIndicator() {
                public boolean isCanceled() {
                    return ticker.isCanceled();
                }
            };

            List<Issue> issues = rv.validate(r, CheckMode.ALL, cancelMonitor);
            for (Issue issue : issues) {
                addIssueDiagnostic(diagnostics, issue, f, f.getParentFile());
            }
            worked(ticker, 1);
            ppRunner.tearDown();
        } catch (Exception e) {
            addExceptionDiagnostic(diagnostics, "Internal Error, failed to setUp PPDiagnostic.", e);
        }
        return r;
    }

    /**
     * TODO: Horribly long method that should be refactored into several to get better optimization.
     * 
     * @param diagnostics
     * @param root
     * @param options
     * @param examinedFiles
     * @param monitor
     * @return
     */
    private BuildResult validateDirectory(Diagnostic diagnostics, File root, ValidationOptions options,
            File[] examinedFiles, IProgressMonitor monitor) {

        if (!(options.getFileType() == FileType.PUPPET_ROOT || options.getFileType() == FileType.MODULE_ROOT))
            throw new IllegalArgumentException("doDir can only process PUPPET_ROOT or MODULE_ROOT");

        // Process request to check layout
        if (options.isCheckLayout()) {
            if (options.getFileType() == FileType.MODULE_ROOT)
                checkModuleLayout(diagnostics, root, root);
            else if (options.getFileType() == FileType.PUPPET_ROOT)
                checkPuppetRootLayout(diagnostics, root, root);
        }

        List<File> ppFiles = findPPFiles(root);
        List<File> rbFiles = findRubyFiles(root);
        Collection<File> mdRoots = forge.findModuleRoots(root, null);
        List<File> rakeFiles = findRakefiles(root);

        final int workload = ppFiles.size() + mdRoots.size() * 3 + rbFiles.size() * 2 //
                + rakeFiles.size() * 2 //
                + 1 // load pptp
                + 1 // "for the pot" (to make sure there is a final tick to report)
        ;

        final SubMonitor ticker = SubMonitor.convert(monitor, workload); // TODO: scaling

        PPDiagnosticsRunner ppRunner = new PPDiagnosticsRunner();
        RubyHelper rubyHelper = new RubyHelper();

        try {
            IValidationAdvisor.ComplianceLevel complianceLevel = options.getComplianceLevel();
            if (complianceLevel == null)
                complianceLevel = IValidationAdvisor.ComplianceLevel.PUPPET_2_7;
            IPotentialProblemsAdvisor problemsAdvisor = options.getProblemsAdvisor();
            if (problemsAdvisor == null)
                problemsAdvisor = new DefaultPotentialProblemsAdvisor();
            ppRunner.setUp(complianceLevel, problemsAdvisor);
            rubyHelper.setUp();
        } catch (Exception e) {
            addExceptionDiagnostic(diagnostics, "Internal Error: Exception while setting up diagnostics.", e);
            return new BuildResult(rubyHelper.isRubyServicesAvailable()); // give up
        }
        ppRunner.configureEncoding(options.getEncodingProvider());
        ppRunner.configureSearchPath(root, options.getSearchPath(), options.getEnvironment());

        // get the configured search path
        final PPSearchPath searchPath = ppRunner.getDefaultSearchPath();

        // Modulefile processing
        // Modulefiles must be processed first in order to figure out containers and container visibility.
        final IPath rootPath = new Path(root.getAbsolutePath());
        final IPath nodeRootPath = rootPath.append(NAME_OF_DIR_WITH_RESTRICTED_SCOPE);

        // collect info in a structure
        Multimap<ModuleName, MetadataInfo> moduleData = ArrayListMultimap.create();
        for (File mdRoot : mdRoots) {
            // load and remember all that loaded ok
            File[] mdProvider = new File[1];
            Metadata m;
            try {
                m = forge.createFromModuleDirectory(mdRoot, true, null, mdProvider, diagnostics);
            } catch (IOException e) {
                addFileError(diagnostics, mdProvider[0], mdRoot, "Can not parse file: " + e.getMessage(),
                        IValidationConstants.ISSUE__MODULEFILE_PARSE_ERROR);
                m = null;
            }
            if (m == null)
                worked(ticker, 1);
            else {
                File f = mdProvider[0];
                ModuleName moduleName = m.getName();
                if (options.isCheckModuleSemantics()
                        && isOnPath(pathToFile(f.getAbsolutePath(), root), searchPath)) {
                    // remember the metadata and where it came from
                    // and if it represents a NODE as opposed to a regular MODULE
                    moduleData.put(moduleName,
                            new MetadataInfo(m, f, nodeRootPath.isPrefixOf(new Path(f.getAbsolutePath()))));
                }
                if (isValidationWanted(examinedFiles, f)) {
                    validateModuleMetadata(m, diagnostics, f, root, options, ticker.newChild(1));
                } else
                    worked(ticker, 1);
            }
        }

        if (options.isCheckModuleSemantics()) {
            for (ModuleName key : moduleData.keySet()) {
                // check there is only one version of each module
                Collection<MetadataInfo> versions = moduleData.get(key);
                boolean redeclared = versions.size() > 1;

                for (MetadataInfo info : versions) {
                    // processed dependencies for one version of a modulefile (in case of errors, there may not be as many ticks as
                    // originally requested)
                    // this ticks before the fact (but there is "one for the pot" left at the end),
                    // as this makes it easier to just do "continue" below.
                    worked(ticker, 1);

                    // skip checks for unwanted
                    final boolean shouldDiagnosticBeReported = isValidationWanted(examinedFiles, info.getFile());
                    // if(!) continue;

                    if (redeclared && shouldDiagnosticBeReported) {
                        addFileError(diagnostics, info.getFile(), root,
                                "Redefinition - equally named already exists",
                                IValidationConstants.ISSUE__MODULEFILE_REDEFINITION);
                    }
                    // Resolve all dependencies
                    for (Dependency d : info.getMetadata().getDependencies()) {

                        // check dependency name and version requirement
                        final ModuleName requiredName = d.getName();
                        if (requiredName == null) {
                            if (shouldDiagnosticBeReported)
                                addFileError(diagnostics, info.getFile(), root, "Dependency without name",
                                        IValidationConstants.ISSUE__MODULEFILE_DEPENDENCY_ERROR);
                            continue; // not meaningful to resolve this
                                      // dependency
                        }

                        // find the best candidate (ignore the fact that there should just be one version of each
                        // module - there may be several, and one of the match).
                        // It is allowed to have modules without versions, they can only be matched by
                        // a dependency that does not have a version requirement.
                        //
                        Collection<MetadataInfo> candidates = moduleData.get(requiredName);
                        List<Version> candidateVersions = Lists.newArrayList();
                        List<MetadataInfo> unversioned = Lists.newArrayList();
                        if (candidates != null)
                            for (MetadataInfo mi : candidates) {
                                Version cv = mi.getMetadata().getVersion();
                                if (cv == null) {
                                    unversioned.add(mi);
                                    continue; // the (possibly) broken version
                                              // is reported elsewhere
                                }
                                candidateVersions.add(cv);
                            }

                        // if the dependency has no version requirement use ">=0"
                        final VersionRange versionRequirement = d.getVersionRequirement();
                        if (versionRequirement == null) {
                            // find best match for >= 0 if there are candidates with versions
                            // the best will always win over unversioned.
                            if (candidateVersions.size() > 0) {
                                Collections.sort(candidateVersions);
                                Version best = candidateVersions.get(candidateVersions.size() - 1);

                                // get the matched MetaDataInfo as the resolution of the dependency
                                // and remember it
                                for (MetadataInfo mi : candidates) {
                                    if (mi.getMetadata().getVersion().equals(best))
                                        info.addResolvedDependency(d, mi);
                                }

                            }
                            // or there must be unversioned candidates
                            else if (unversioned.size() == 0)
                                if (shouldDiagnosticBeReported)
                                    addFileDiagnostic(diagnostics,
                                            (candidates.size() > 0 ? Diagnostic.WARNING : Diagnostic.ERROR),
                                            info.getFile(), root,
                                            "Unresolved Dependency to: " + d.getName() + " (unversioned).",
                                            IValidationConstants.ISSUE__MODULEFILE_UNSATISFIED_DEPENDENCY);
                                else {
                                    // pick the first as resolution
                                    // worry about ambiguity elsewhere
                                    info.addResolvedDependency(d, unversioned.get(0));
                                }
                        } else {
                            // there was a version requirement, it must match something with a version.
                            Version best = d.getVersionRequirement().findBestMatch(candidateVersions);
                            if (best == null) {
                                info.addUnresolvedDependency(d);
                                if (shouldDiagnosticBeReported)
                                    addFileDiagnostic(diagnostics,
                                            (candidates.size() > 0 ? Diagnostic.WARNING : Diagnostic.ERROR),
                                            info.getFile(), root,
                                            "Unresolved Dependency to: " + d.getName() + " version: "
                                                    + d.getVersionRequirement(),
                                            IValidationConstants.ISSUE__MODULEFILE_UNSATISFIED_DEPENDENCY);
                            } else {
                                // get the matched MetaDataInfo as the resolution of the dependency
                                // and remember it
                                for (MetadataInfo mi : candidates) {
                                    if (mi.getMetadata().getVersion().equals(best))
                                        info.addResolvedDependency(d, mi);
                                }
                            }
                        }
                    }
                }
            }
            IPotentialProblemsAdvisor advisor = options.getProblemsAdvisor();
            if (advisor != null && advisor.circularDependencyPreference().isWarningOrError()) {
                ValidationPreference preference = options.getProblemsAdvisor().circularDependencyPreference();
                checkCircularDependencies(moduleData, diagnostics, root);
                for (MetadataInfo mi : moduleData.values()) {
                    if (isValidationWanted(examinedFiles, mi.getFile())) {
                        for (String circularity : mi.getCircularityMessages())
                            addFileDiagnostic(diagnostics,
                                    preference.isError() ? Diagnostic.ERROR : Diagnostic.WARNING, mi.getFile(),
                                    root, circularity, IPPDiagnostics.ISSUE__CIRCULAR_MODULE_DEPENDENCY);
                    }
                }
            }
        }
        // TODO: Wasteful to calculate the URL's more than once.
        // Could be done once per pp and rb (to separate the processing), or have all in one pile
        // and let processing look at extension.

        // Calculate containers
        // sets up iterateable over all files including pptp

        boolean useContainers = true;
        URI uri = options.getPlatformURI();
        if (useContainers) {
            List<URI> pptpURIs = Lists.newArrayList(uri != null ? uri : PuppetTarget.getDefault().getPlatformURI());
            ppRunner.configureContainers(root, moduleData.values(), //
                    Iterables.concat(
                            Iterables.transform(Iterables.concat(ppFiles, rbFiles), new Function<File, URI>() {
                                @Override
                                public URI apply(File from) {
                                    return URI.createFileURI(from.getPath());
                                }
                            }), pptpURIs));
        }
        // Load pptp
        if (options.isCheckReferences()) {
            try {
                URI platformURI = options.getPlatformURI();
                ppRunner.loadResource(
                        platformURI != null ? platformURI : PuppetTarget.getDefault().getPlatformURI());
            } catch (IOException e) {
                addExceptionDiagnostic(diagnostics, "Internal Error: Could not load pptp.", e);
                return new BuildResult(rubyHelper.isRubyServicesAvailable()); // give
                // up
            }
        }
        worked(ticker, 1);

        // Load all ruby
        for (File f : rbFiles) {
            try {
                // Skip "Rakefile.rb" or they will be processed twice (but still tick x2
                // onece for validate and once for load - as this is included in work-count)
                if (f.getName().toLowerCase().equals("rakefile.rb")) {
                    worked(ticker, 2);
                    continue;
                }
                // Syntax check ruby file
                // consumes one rb tick
                if (isValidationWanted(examinedFiles, f))
                    validateRubyFile(rubyHelper, diagnostics, f, root, ticker.newChild(1));
                else
                    worked(ticker, 1);

                // Load ruby file with pptp contribution
                // consumes one rb tick
                if (options.isCheckReferences()) {
                    Resource r = ppRunner.loadResource(new FileInputStream(f), URI.createFileURI(f.getPath()));
                    if (r != null)
                        rememberRootInResource(root, r);
                }
                worked(ticker, 1);
            } catch (Exception e) {
                addExceptionDiagnostic(diagnostics,
                        "Internal Error: Exception while processing file: " + f.getName() + ": " + e, e);
                e.printStackTrace();
            }
        }
        RakefileInfo rakefileInfo = new RakefileInfo();
        // System.err.println("Processing Rakefiles count: " + rakeFiles.size());

        for (File f : rakeFiles) {
            // Syntax check ruby file
            // consumes one rakefile tick
            if (isValidationWanted(examinedFiles, f))
                validateRubyFile(rubyHelper, diagnostics, f, root, ticker.newChild(1));
            else
                worked(ticker, 1);

            // parsing adds one rakefile work tick
            rakefileInfo.addRakefile(getRakefileInformation(rubyHelper, f, root, ticker.newChild(1)));

        }
        // Load all pp
        // crosslink and validate all
        Map<File, Resource> ppResources = Maps.newHashMapWithExpectedSize(ppFiles.size());
        for (File f : ppFiles) {
            try {
                ppResources.put(f, ppRunner.loadResource(new FileInputStream(f), URI.createFileURI(f.getPath())));
            } catch (IOException e) {
                addExceptionDiagnostic(diagnostics, "I/O Error: Exception while processing file: " + f.toString(),
                        e);
            } catch (Exception e) {
                addExceptionDiagnostic(diagnostics,
                        "Internal Error: Exception while processing file: " + f.toString(), e);
            }
            // consume one pp tick
            worked(ticker, 1);
        }

        // Must set the root in all resources to allow cross reference error reports to contain
        // relative paths
        for (Resource r : ppResources.values())
            rememberRootInResource(root, r);

        IResourceValidator validator = ppRunner.getPPResourceValidator();
        long maxLinkTime = 0;
        // Turn on for debugging particular files
        // File slowCandidate = new File("/Users/henrik/gitrepos/forge-modules/jeffmccune-mockbuild/manifests/init.pp");

        for (Entry<File, Resource> r : ppResources.entrySet()) {
            File f = r.getKey();
            if (!isValidationWanted(examinedFiles, f))
                continue;
            long beforeTime = System.currentTimeMillis();
            boolean profileThis = false; // /* for debugging slow file */
            // f.equals(slowCandidate);
            if (options.isCheckReferences())
                ppRunner.resolveCrossReferences(r.getValue(), profileThis, ticker);
            long afterTime = System.currentTimeMillis();
            if (afterTime - beforeTime > maxLinkTime) {
                maxLinkTime = afterTime - beforeTime;
            }
            final CancelIndicator cancelMonitor = new CancelIndicator() {
                public boolean isCanceled() {
                    return ticker.isCanceled();
                }
            };

            List<Issue> issues = validator.validate(r.getValue(), CheckMode.ALL, cancelMonitor);
            for (Issue issue : issues) {
                addIssueDiagnostic(diagnostics, issue, f, root);
            }
        }
        // // Debug stuff
        // if(slowestFile != null)
        // System.err.printf("Slowest file =%s (%s)\n", slowestFile.getAbsolutePath(), maxLinkTime);

        // // Compute the returned map
        // // Only the restricted modules are wanted (as everything else sees everything)
        // Iterable<File> filteredMdFiles = Iterables.filter(mdFiles, new Predicate<File>() {
        //
        // @Override
        // public boolean apply(File input) {
        // IPath p = new Path(input.getPath());
        // if(p.segmentCount() < 3)
        // return false;
        // p = p.removeLastSegments(2);
        // return NAME_OF_DIR_WITH_RESTRICTED_SCOPE.equals(p.lastSegment());
        // }
        // });
        AllModuleReferences all = ppRunner.getAllModulesState();

        // set the root to allow relative lookup of module exports
        all.setRoot(root);

        // // Debug stuff...
        // for(File f : result.getMap().keySet()) {
        // System.err.println("Exports for file: " + f.toString());
        // for(ExportsPerModule.Export export : result.getMap().get(f)) {
        // System.err.printf(
        // "    %s, %s, %s\n", export.getName(), export.getEClass().getName(),
        // export.getParentName());
        // }
        // }
        ppRunner.tearDown();
        boolean rubyServicesAvailable = rubyHelper.isRubyServicesAvailable();
        rubyHelper.tearDown();
        // make sure everything is consumed
        ticker.setWorkRemaining(0);
        BuildResult buildResult = new BuildResult(rubyServicesAvailable);
        // buildResult.setExportsForNodes(result);
        buildResult.setAllModuleReferences(all);
        buildResult.setModuleData(moduleData);
        buildResult.setRakefileInfo(rakefileInfo);
        return buildResult;
    }

    public void validateManifest(Diagnostic diagnostics, File sourceFile, IProgressMonitor monitor) {
        ValidationOptions options = new ValidationOptions();
        options.setCheckLayout(false);
        options.setCheckModuleSemantics(false);
        options.setCheckReferences(false);
        options.setFileType(FileType.SINGLE_SOURCE_FILE);

        validate(diagnostics, sourceFile, options, null, monitor);
    }

    /**
     * @deprecated use {@link #validate(DiagnosticChain, String)} instead.
     */
    @Deprecated
    public Resource validateManifest(Diagnostic diagnostics, String code, IProgressMonitor monitor) {
        return validate(diagnostics, code, monitor);
    }

    public BuildResult validateModule(Diagnostic diagnostics, File moduleRoot, IProgressMonitor monitor) {
        ValidationOptions options = new ValidationOptions();
        options.setCheckLayout(true);
        options.setCheckModuleSemantics(false);
        options.setCheckReferences(false);
        options.setFileType(FileType.MODULE_ROOT);

        return validate(diagnostics, moduleRoot, options, null, monitor);
    }

    /**
     * Validates a Modulefile by a) loading it, and b) validating the metadata
     * 
     * @param diagnostics
     * @param source
     * @param parentFile
     */
    private void validateModulefile(Diagnostic diagnostics, File parentFile, ValidationOptions options,
            IProgressMonitor monitor) {
        SubMonitor ticker = SubMonitor.convert(monitor, 11);
        File[] mdProvider = new File[1];
        Metadata metadata = loadModuleMetadata(diagnostics, parentFile, mdProvider, ticker.newChild(1));
        if (metadata == null)
            return; // failed in some way and should have reported this

        validateModuleMetadata(metadata, diagnostics, mdProvider[0], parentFile, options, ticker.newChild(10));
    }

    /**
     * Validate module meta data if options has isCheckModuleSemantics (otherwise this validation is a no-op). A tick is
     * produced on the monitor even if validation is not wanted. NOTE: This method does not check dependency resolution.
     * 
     * @param metadata
     * @param diagnostics
     * @param moduleFile
     * @param parentFile
     * @param options
     * @param monitor
     */
    private void validateModuleMetadata(Metadata metadata, Diagnostic diagnostics, File moduleFile, File parentFile,
            ValidationOptions options, IProgressMonitor monitor) {

        SubMonitor ticker = SubMonitor.convert(monitor, 1);
        if (options.isCheckModuleSemantics()) {

            // must have name
            ModuleName moduleName = metadata.getName();
            if (moduleName == null)
                addFileError(diagnostics, moduleFile, parentFile, "A name must be specified.",
                        IValidationConstants.ISSUE__MODULEFILE_NO_NAME);

            // must have version
            Version version = metadata.getVersion();
            if (version == null)
                addFileWarning(diagnostics, moduleFile, parentFile, "A version should be specified.",
                        IValidationConstants.ISSUE__MODULEFILE_NO_VERSION);
        }
        worked(ticker, 1);

    }

    private void validatePPFile(PPDiagnosticsRunner dr, Diagnostic diagnostics, File f, File root,
            IProgressMonitor monitor) {
        final SubMonitor ticker = SubMonitor.convert(monitor, 2);
        worked(ticker, 1);
        try {
            FileInputStream input = new FileInputStream(f);
            Resource r = dr.loadResource(input, URI.createFileURI(f.getPath()));
            IResourceValidator rv = dr.getPPResourceValidator();
            final CancelIndicator cancelMonitor = new CancelIndicator() {
                public boolean isCanceled() {
                    return ticker.isCanceled();
                }
            };

            List<Issue> issues = rv.validate(r, CheckMode.ALL, cancelMonitor);
            worked(ticker, 1);
            for (Issue issue : issues) {
                addIssueDiagnostic(diagnostics, issue, f, root);
            }
        } catch (Exception e) {
            addExceptionDiagnostic(diagnostics, "Internal Error: Exception while processing file: " + f.toString(),
                    e);
        }
    }

    public void validateRepository(Diagnostic diagnostics, File catalogRoot, File factorData, File siteFile,
            String nodeName, IProgressMonitor monitor) {
        if (diagnostics == null)
            throw new IllegalArgumentException("diagnostics can not be null");
        if (catalogRoot == null)
            throw new IllegalArgumentException("catalogRoot can not be null");
        if (factorData == null)
            throw new IllegalArgumentException("factorData can not be null");
        if (siteFile == null)
            throw new IllegalArgumentException("siteFile can not be null");
        if (nodeName == null || nodeName.length() < 1)
            throw new IllegalArgumentException("nodeName can not be null or empty");

        assertFileAvailability(factorData, false);
        assertFileAvailability(siteFile, false);
        assertFileAvailability(catalogRoot, true);

        final SubMonitor ticker = SubMonitor.convert(monitor, 2000);
        // perform static validation first
        validateRepository(diagnostics, catalogRoot, ticker.newChild(1000));

        // check for early exit due to cancel or errors
        int severity = diagnostics.getSeverity();
        if (ticker.isCanceled() || severity > Diagnostic.WARNING)
            return;

        // perform a catalog production
        PuppetCatalogCompilerRunner runner = new PuppetCatalogCompilerRunner();
        runner.compileCatalog(siteFile, catalogRoot, nodeName, factorData, ticker.newChild(1000));
        diagnostics.addChildren(runner.getDiagnostics());
    }

    public BuildResult validateRepository(Diagnostic diagnostics, File catalogRoot, IProgressMonitor monitor) {
        ValidationOptions options = new ValidationOptions();
        options.setCheckLayout(true);
        options.setCheckModuleSemantics(true);
        options.setCheckReferences(true);
        options.setFileType(FileType.PUPPET_ROOT);

        return validate(diagnostics, catalogRoot, options, null, monitor);
    }

    private void validateRubyFile(RubyHelper rubyHelper, Diagnostic diagnostics, File f, File root,
            IProgressMonitor monitor) {
        SubMonitor ticker = SubMonitor.convert(monitor, 1);
        try {
            IRubyParseResult result = rubyHelper.parse(f);
            for (IRubyIssue issue : result.getIssues()) {
                addRubyIssueDiagnostic(diagnostics, issue, f, root);
            }
        } catch (Exception e) {
            addExceptionDiagnostic(diagnostics, "Internal Error: Exception while processing file: " + f.toString(),
                    e);
        }
        worked(ticker, 1);
    }

    private void worked(SubMonitor monitor, int amount) throws OperationCanceledException {
        if (monitor.isCanceled())
            throw new OperationCanceledException();
        monitor.worked(amount);
    }
}