Package com.puppetlabs.geppetto.pp.dsl.ui.builder

Source Code of com.puppetlabs.geppetto.pp.dsl.ui.builder.PPModuleMetadataBuilder

/**
* Copyright (c) 2013 Puppet Labs, 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:
*   Puppet Labs
*/

package com.puppetlabs.geppetto.pp.dsl.ui.builder;

import static com.puppetlabs.geppetto.forge.Forge.METADATA_JSON_NAME;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
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.xtext.ui.XtextProjectHelper;
import org.eclipse.xtext.util.Wrapper;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Lists;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.puppetlabs.geppetto.common.tracer.DefaultTracer;
import com.puppetlabs.geppetto.common.tracer.ITracer;
import com.puppetlabs.geppetto.common.tracer.NullTracer;
import com.puppetlabs.geppetto.diagnostic.Diagnostic;
import com.puppetlabs.geppetto.diagnostic.FileDiagnostic;
import com.puppetlabs.geppetto.forge.FilePosition;
import com.puppetlabs.geppetto.forge.Forge;
import com.puppetlabs.geppetto.forge.model.Dependency;
import com.puppetlabs.geppetto.forge.model.Metadata;
import com.puppetlabs.geppetto.forge.model.ModuleName;
import com.puppetlabs.geppetto.pp.dsl.ui.PPUiConstants;
import com.puppetlabs.geppetto.pp.dsl.ui.internal.PPDSLActivator;
import com.puppetlabs.geppetto.pp.dsl.validation.IValidationAdvisor;
import com.puppetlabs.geppetto.semver.Version;
import com.puppetlabs.geppetto.semver.VersionRange;

/**
* Builder of Modulefile/metadata.json.
* This builder performs the following tasks:
* <ul>
* <li>sets the dependencies on the project as dynamic project references</li>
* <li>ensure all puppet projects have a dynamic reference to the target project</li>
* <li>keeps derived metadata.json in sync with Modulefile if applicable (content and checksums)</li>
* </ul>
*
*/

public class PPModuleMetadataBuilder extends IncrementalProjectBuilder implements PPUiConstants {
  private final static Logger log = Logger.getLogger(PPModuleMetadataBuilder.class);

  /**
   * Returns the best matching project (or null if there is no match) among the projects in the
   * workspace
   *
   * @param name
   *            The name of the module to match
   * @param versionRequirement
   *            The version requirement. Can be <code>null</code>.
   * @return The matching project or <code>null</code>.
   */
  public static IProject getBestMatchingProject(ModuleName requiredName, VersionRange versionRequirement) {
    return getBestMatchingProject(requiredName, versionRequirement, NullTracer.INSTANCE);
  }

  private static IProject getBestMatchingProject(ModuleName name, VersionRange vr, ITracer tracer) {
    // Names with "/" are not allowed
    if(name == null) {
      if(tracer.isTracing())
        tracer.trace("Dependency with missing name found");
      return null;
    }

    if(tracer.isTracing())
      tracer.trace("Resolving required name: ", name);
    BiMap<IProject, Version> candidates = HashBiMap.create();

    if(tracer.isTracing())
      tracer.trace("Checking against all projects...");
    for(IProject p : getWorkspaceRoot().getProjects()) {
      if(!isAccessiblePuppetProject(p)) {
        if(tracer.isTracing())
          tracer.trace("Project not accessible: ", p.getName());
        continue;
      }

      Version version = null;
      ModuleName moduleName = null;
      try {
        String mn = p.getPersistentProperty(PROJECT_PROPERTY_MODULENAME);
        moduleName = mn == null
            ? null
            : ModuleName.fromString(mn);
      }
      catch(CoreException e) {
        log.error("Could not read project Modulename property", e);
      }
      if(tracer.isTracing())
        tracer.trace("Project: ", p.getName(), " has persisted name: ", moduleName);
      boolean matched = false;
      if(name.equals(moduleName))
        matched = true;

      if(tracer.isTracing()) {
        if(!matched)
          tracer.trace("== not matched on name");
      }
      // get the version from the persisted property
      if(matched) {
        try {
          version = Version.fromString(p.getPersistentProperty(PROJECT_PROPERTY_MODULEVERSION));
        }
        catch(Exception e) {
          log.error("Error while getting version from project", e);
        }
        if(version == null)
          version = Version.MIN;
        if(tracer.isTracing())
          tracer.trace("Candidate with version; ", version.toString(), " added as candidate");
        candidates.put(p, version);
      }
    }
    if(candidates.isEmpty()) {
      if(tracer.isTracing())
        tracer.trace("No candidates found");
      return null;
    }
    if(tracer.isTracing()) {
      tracer.trace("Getting best version");
    }
    // find best version and do a lookup of project
    if(vr == null)
      vr = VersionRange.ALL_INCLUSIVE;
    Version best = vr.findBestMatch(candidates.values());
    if(best == null) {
      if(tracer.isTracing())
        tracer.trace("No best match found");
      return null;
    }
    if(tracer.isTracing()) {
      tracer.trace("Found best project: ", candidates.inverse().get(best).getName(), "having version:", best);
    }
    return candidates.inverse().get(best);
  }

  private static int getMarkerSeverity(Diagnostic diagnostic) {
    int markerSeverity;
    switch(diagnostic.getSeverity()) {
      case Diagnostic.FATAL:
      case Diagnostic.ERROR:
        markerSeverity = IMarker.SEVERITY_ERROR;
        break;
      case Diagnostic.WARNING:
        markerSeverity = IMarker.SEVERITY_WARNING;
        break;
      default:
        markerSeverity = IMarker.SEVERITY_INFO;
    }
    return markerSeverity;
  }

  private static IWorkspaceRoot getWorkspaceRoot() {
    return ResourcesPlugin.getWorkspace().getRoot();
  }

  /**
   * TODO: Should check for the Puppet Nature instead of Xtext
   *
   * @param p
   * @return
   */
  private static boolean isAccessiblePuppetProject(IProject p) {
    return p != null && XtextProjectHelper.hasNature(p);
  }

  private final Provider<IValidationAdvisor> validationAdvisorProvider;

  private Forge forge;

  private ITracer tracer;

  private IValidationAdvisor validationAdvisor;

  public PPModuleMetadataBuilder() {
    // Hm, can not inject this because it was not possible to inject this builder via the
    // executable extension factory
    Injector injector = ((PPDSLActivator) PPDSLActivator.getInstance()).getPPInjector();
    tracer = new DefaultTracer(PPUiConstants.DEBUG_OPTION_MODULEFILE);
    validationAdvisorProvider = injector.getProvider(IValidationAdvisor.class);
    forge = injector.getInstance(Forge.class);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.resources.IncrementalProjectBuilder#build(int, java.util.Map, org.eclipse.core.runtime.IProgressMonitor)
   */
  @Override
  protected IProject[] build(int kind, @SuppressWarnings("rawtypes") Map args, IProgressMonitor monitor)
      throws CoreException {
    SubMonitor subMon = SubMonitor.convert(monitor);
    checkCancel(subMon);

    IResourceDelta delta = getDelta(getProject());
    if(delta == null)
      fullBuild(subMon);
    else
      incrementalBuild(delta, subMon);
    return null;
  }

  private void checkCancel(SubMonitor monitor) throws OperationCanceledException {
    if(monitor.isCanceled()) {
      forgetLastBuiltState();
      throw new OperationCanceledException();
    }
  }

  private void checkCircularDependencies() {
    IProject p = getProject();
    List<IProject> visited = Lists.newArrayList();
    List<IProject> circular = Lists.newArrayList();
    try {
      visit(p, visited, circular);
    }
    catch(CoreException e) {
      log.error("Can not check for circular dependencies", e);
    }
    isCircular: if(!circular.isEmpty()) {
      // a direct dependency A -> A is tolerated for the hidden PPTP project
      if(circular.get(0).equals(p) && circular.size() == 1 &&
          PPUiConstants.PPTP_TARGET_PROJECT_NAME.equals(p.getName()))
        break isCircular;

      StringBuffer buf = new StringBuffer("Circular dependency: [");
      for(IProject circ : circular) {
        buf.append(circ.getName());
        buf.append("->");
      }
      buf.append("]");
      int circularSeverity = -1;
      switch(getValidationAdvisor().circularDependencyPreference()) {
        case ERROR:
          circularSeverity = IMarker.SEVERITY_ERROR;
          break;
        case WARNING:
          circularSeverity = IMarker.SEVERITY_WARNING;
          break;
        case IGNORE: // just don't do it...
          break;
      }

      if(circularSeverity != -1)
        createMarker(circularSeverity, p, buf.toString(), null);
    }

  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.resources.IncrementalProjectBuilder#clean(org.eclipse.core.runtime.IProgressMonitor)
   */
  @Override
  protected void clean(IProgressMonitor monitor) throws CoreException {
    removeErrorMarkers();
    // potentially remove the dynamic project references, but this will probably trigger project reconfig
    // better to wait until the sync as they are probably still the same
    if(monitor != null)
      monitor.done();
  }

  private void createErrorMarker(IResource r, String message, Dependency d) {
    createMarker(IMarker.SEVERITY_ERROR, r, message, d);
  }

  private void createMarker(int severity, IResource r, String message, Dependency d) {
    try {
      IMarker m = r.createMarker(PUPPET_MODULE_PROBLEM_MARKER_TYPE);
      m.setAttribute(IMarker.MESSAGE, message);
      m.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
      m.setAttribute(IMarker.SEVERITY, severity);
      if(d != null) {
        VersionRange vr = d.getVersionRequirement();
        m.setAttribute(IMarker.LOCATION, d.getName() + (vr == null
            ? ""
            : vr.toString()));
        if(d instanceof FilePosition)
          m.setAttribute(IMarker.LINE_NUMBER, ((FilePosition) d).getLine() + 1);
      }
      else
        m.setAttribute(IMarker.LOCATION, r.getName());
    }
    catch(CoreException e) {
      log.error("Could not create error marker or set its attributes", e);
    }

  }

  private void createResourceMarkers(IResource r, Diagnostic diagnostic) {
    for(Diagnostic child : diagnostic)
      createResourceMarkers(r, child);

    String msg = diagnostic.getMessage();
    if(msg == null)
      return;

    try {
      IMarker m = r.createMarker(PUPPET_MODULE_PROBLEM_MARKER_TYPE);
      m.setAttribute(IMarker.MESSAGE, msg);
      m.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
      m.setAttribute(IMarker.SEVERITY, getMarkerSeverity(diagnostic));
      m.setAttribute(IMarker.LOCATION, r.getName());
      if(diagnostic instanceof FileDiagnostic)
        m.setAttribute(IMarker.LINE_NUMBER, ((FileDiagnostic) diagnostic).getLineNumber());
    }
    catch(CoreException e) {
      log.error("Could not create error marker or set its attributes", e);
    }
  }

  private void createWarningMarker(IResource r, String message, Dependency d) {
    createMarker(IMarker.SEVERITY_WARNING, r, message, d);
  }

  private void fullBuild(SubMonitor monitor) {
    removeErrorMarkers();
    syncModuleMetadata(monitor);
  }

  private IProject getProjectByName(String name) {
    return getProject().getWorkspace().getRoot().getProject(name);
  }

  private synchronized IValidationAdvisor getValidationAdvisor() {
    if(validationAdvisor == null)
      validationAdvisor = validationAdvisorProvider.get();
    return validationAdvisor;
  }

  private void incrementalBuild(IResourceDelta delta, final SubMonitor monitor) {
    removeErrorMarkers();
    final Wrapper<Boolean> buildFlag = Wrapper.wrap(Boolean.FALSE);

    try {
      delta.accept(new IResourceDeltaVisitor() {

        public boolean visit(IResourceDelta delta) throws CoreException {
          checkCancel(monitor);

          if(buildFlag.get())
            return false; // already convinced we should build
          if(delta.getResource() != null && delta.getResource().isDerived())
            return false;

          // irrespective of how the Modulefile/metadata.json was changed, run the build (i.e. sync).
          if(isModuleMetadataDelta(delta))
            buildFlag.set(true);

          // if any file included in the checksum was changed, added, removed etc.
          if(isChecksumChange(delta))
            buildFlag.set(true);

          // continue scanning the delta tree
          return true;
        }
      });
      if(buildFlag.get())
        syncModuleMetadata(monitor);
    }
    catch(CoreException e) {
      log.error(e.getMessage(), e);
      syncModuleMetadata(monitor);
    }
  }

  /**
   * A change to any file except &quot;metadata.json&quot; is a checksum change.
   *
   * @param delta
   * @return
   */
  private boolean isChecksumChange(IResourceDelta delta) {
    IResource resource = delta.getResource();
    if(!(resource instanceof IFile))
      return false;
    if(resource.getProjectRelativePath().equals(METADATA_JSON_PATH))
      return false;
    return true; // all other files are included in the checksum list.
  }

  private boolean isModuleMetadataDelta(IResourceDelta delta) {
    IResource resource = delta.getResource();
    if(resource instanceof IFile && !resource.isDerived()) {
      IPath relPath = delta.getProjectRelativePath();
      return MODULEFILE_PATH.equals(relPath) || METADATA_JSON_PATH.equals(relPath);
    }
    return false;
  }

  /**
   * Deletes all problem markers set by this builder.
   */
  private void removeErrorMarkers() {
    IFile m = getProject().getFile(MODULEFILE_PATH);
    try {
      if(m.exists())
        m.deleteMarkers(PUPPET_MODULE_PROBLEM_MARKER_TYPE, true, IResource.DEPTH_ZERO);
    }
    catch(CoreException e) {
      // nevermind, the resource may not even be there...
      // meaningless to have elaborate existence checks etc...
    }

    m = getProject().getFile(METADATA_JSON_PATH);
    try {
      if(!m.isDerived() && m.exists())
        m.deleteMarkers(PUPPET_MODULE_PROBLEM_MARKER_TYPE, true, IResource.DEPTH_ZERO);
    }
    catch(CoreException e) {
    }

    try {
      getProject().deleteMarkers(PUPPET_MODULE_PROBLEM_MARKER_TYPE, true, IResource.DEPTH_ZERO);
    }
    catch(CoreException e) {
    }
  }

  /**
   * Resolves the dependencies against projects in the workspace.
   * Sets error markers when there are unresolved dependencies.
   *
   * @param handle
   * @return
   */
  private List<IProject> resolveDependencies(Metadata metadata, IFile moduleMetadataFile) {
    List<IProject> result = Lists.newArrayList();

    // parse the 'Modulefile' or 'metadata.json' and get full name and version, use this as name of target entry
    try {
      for(Dependency d : metadata.getDependencies()) {
        IProject best = getBestMatchingProject(d.getName(), d.getVersionRequirement(), tracer);
        if(best != null) {
          if(!result.contains(best))
            result.add(best);
        }
        else {
          VersionRange vr = d.getVersionRequirement();
          createErrorMarker(moduleMetadataFile, "Unresolved dependency :'" + d.getName() + (vr == null
              ? ""
              : ("' version: " + vr)), d);
        }
      }
    }
    catch(Exception e) {
      if(log.isDebugEnabled())
        log.debug("Error while resolving dependencies: '" + moduleMetadataFile + "'", e);
    }
    return result;
  }

  private void syncModulefileAndReferences(final SubMonitor subMon) {
    subMon.setWorkRemaining(4);
    try {
      IProject project = getProject();
      if(!isAccessiblePuppetProject(project))
        return;

      if(tracer.isTracing())
        tracer.trace("Syncing modulefile with project");

      File projectDir = project.getLocation().toFile();
      if(!forge.hasModuleMetadata(projectDir, null)) {
        // puppet project without modulefile should reference the target project
        syncProjectReferences(
          Lists.newArrayList(getProjectByName(PPUiConstants.PPTP_TARGET_PROJECT_NAME)), subMon.newChild(7));
        return;
      }

      checkCancel(subMon);

      // get metadata
      Metadata metadata;
      File[] extractionSource = new File[1];
      IFile metadataResource = project.getFile(METADATA_JSON_NAME);
      boolean metadataDerived = metadataResource.isDerived();

      if(metadataDerived) {
        try {
          // Delete this file. It will be recreated from other
          // sources.
          metadataResource.delete(true, subMon.newChild(1));
        }
        catch(CoreException e) {
          log.error("Unable to delete metadata.json", e);
        }
      }
      else {
        // The one that will be created should be considered to
        // be derived
        metadataDerived = !metadataResource.exists();
        subMon.worked(1);
      }

      Diagnostic diagnostic = new Diagnostic();
      try {
        // Load metadata, types, checksums etc.
        metadata = forge.createFromModuleDirectory(projectDir, true, null, extractionSource, diagnostic);
      }
      catch(Exception e) {
        createErrorMarker(project, "Can not parse Modulefile or other metadata source: " + e.getMessage(), null);
        if(log.isDebugEnabled())
          log.debug("Could not parse module description: '" + project.getName() + "'", e);
        return; // give up - errors have been logged.
      }

      if(metadata == null) {
        createErrorMarker(project, "Unable to find Modulefile or other metadata source", null);
        return;
      }

      // Find the resource used for metadata extraction
      File extractionSourceFile = extractionSource[0];
      IPath extractionSourcePath = Path.fromOSString(extractionSourceFile.getAbsolutePath());
      IFile moduleFile = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(extractionSourcePath);

      createResourceMarkers(moduleFile, diagnostic);

      // sync version and name project data
      Version version = null;
      ModuleName moduleName = null;
      if(metadata != null) {
        version = metadata.getVersion();
        moduleName = metadata.getName();
      }

      if(version == null)
        version = Version.fromString("0.0.0");

      if(moduleName != null) {
        if(!project.getName().toLowerCase().contains(moduleName.getName().toString().toLowerCase()))
          createWarningMarker(moduleFile, "Mismatched name - project does not reflect module: '" +
              moduleName + "'", null);
      }

      try {
        IProject p = getProject();
        String storedVersion = p.getPersistentProperty(PROJECT_PROPERTY_MODULEVERSION);
        String vstr = version.toString();
        if(!vstr.equals(storedVersion))
          p.setPersistentProperty(PROJECT_PROPERTY_MODULEVERSION, vstr);

        String storedName = p.getPersistentProperty(PROJECT_PROPERTY_MODULENAME);
        if(moduleName == null) {
          if(storedName != null)
            p.setPersistentProperty(PROJECT_PROPERTY_MODULENAME, null);
        }
        else {
          String mstr = moduleName.toString();
          if(!mstr.equals(storedName))
            p.setPersistentProperty(PROJECT_PROPERTY_MODULENAME, mstr.toString());
        }
      }
      catch(CoreException e1) {
        log.error("Could not set version or symbolic module name of project", e1);
      }

      checkCancel(subMon);
      List<IProject> resolutions = resolveDependencies(metadata, moduleFile);

      // add the TP project
      resolutions.add(getProjectByName(PPUiConstants.PPTP_TARGET_PROJECT_NAME));

      syncProjectReferences(resolutions, subMon.newChild(1));

      // Sync the built .json version
      if(metadataDerived) {
        try {
          // Recreate the metadata.json file
          File mf = new File(project.getLocation().toFile(), METADATA_JSON_NAME);
          forge.saveJSONMetadata(metadata, mf);
          // must refresh the file as it was written outside the resource framework
          try {
            metadataResource.refreshLocal(IResource.DEPTH_ZERO, subMon.newChild(1));
          }
          catch(CoreException e) {
            log.error("Could not refresh 'metadata.json'", e);
          }

          try {
            metadataResource.setDerived(true, subMon.newChild(1));
          }
          catch(CoreException e) {
            log.error("Could not make 'metadata.json' derived", e);
          }

        }
        catch(IOException e) {
          createErrorMarker(moduleFile, "Error while writing 'metadata.json': " + e.getMessage(), null);
          log.error("Could not build 'metadata.json' for: '" + moduleFile + "'", e);
        }
      }
    }
    finally {
      subMon.done();
    }
  }

  private void syncModuleMetadata(final SubMonitor subMon) {
    syncModulefileAndReferences(subMon);
    checkCircularDependencies();
  }

  private void syncProjectReferences(List<IProject> wanted, SubMonitor subMon) {
    subMon.setWorkRemaining(2);
    try {
      checkCancel(subMon);
      final IProject project = getProject();
      project.refreshLocal(IResource.DEPTH_INFINITE, subMon.newChild(1));
      final IProjectDescription description = getProject().getDescription();
      List<IProject> current = Lists.newArrayList(description.getDynamicReferences());
      if(current.size() == wanted.size() && current.containsAll(wanted))
        return; // already in sync
      // not in sync, set them
      IProjectDescription desc = getProject().getDescription();
      desc.setDynamicReferences(wanted.toArray(new IProject[wanted.size()]));
      project.setDescription(desc, subMon.newChild(1));
      // Trigger full rebuild once we're done here
      new PPBuildJob(getWorkspaceRoot().getWorkspace()).schedule();
    }
    catch(CoreException e) {
      log.error("Can not sync project's dynamic dependencies", e);
    }
    finally {
      subMon.done();
    }
  }

  private void visit(IProject p, List<IProject> visited, List<IProject> circular) throws CoreException {
    if(!p.isAccessible())
      return;
    if(visited.contains(p))
      return;
    visited.add(p);
    IProject root = visited.get(0);
    for(IProject dep : p.getReferencedProjects()) {
      if(dep.equals(root)) {
        circular.add(p);
        return;
      }
      visit(dep, visited, circular);
    }

  }
}
TOP

Related Classes of com.puppetlabs.geppetto.pp.dsl.ui.builder.PPModuleMetadataBuilder

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.