blob: 9c26a69f4606734773c92db80d1a41b776eb68d0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 SSI Schaefer IT Solutions GmbH and others.
* 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:
* SSI Schaefer IT Solutions GmbH
*******************************************************************************/
package org.eclipse.tea.library.build.chain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.Execute;
import org.eclipse.e4.core.di.extensions.Service;
import org.eclipse.tea.core.services.TaskProgressTracker;
import org.eclipse.tea.core.services.TaskingLog;
import org.eclipse.tea.library.build.internal.Activator;
import org.eclipse.tea.library.build.services.TeaBuildElementFactory;
import org.eclipse.tea.library.build.services.TeaBuildVisitor;
import org.eclipse.tea.library.build.services.TeaDependencyWireFactory;
import org.eclipse.tea.library.build.services.TeaElementFailurePolicy;
import org.eclipse.tea.library.build.services.TeaElementFailurePolicy.FailurePolicy;
import org.eclipse.tea.library.build.services.TeaElementVisitPolicy;
import org.eclipse.tea.library.build.services.TeaElementVisitPolicy.VisitPolicy;
/**
* Encapsulates "building" of a set of projects, where building consists of:
* <ul>
* <li>Creating to-be-built elements based on a given set of projects from the
* workspace.
* <li>Creating references between these elements and calculating an order to
* build these elements.
* <li>Calling all registered visitors that want to contribute to "building" an
* element.
* </ul>
* <p>
* "Build" in this context could be for instance:
* <ul>
* <li>Compiling a project
* <li>Calling a generator that generates code prior to compiling a project
* <li>...
* </ul>
*/
@SuppressWarnings("restriction")
public class TeaBuildChain {
private final Map<String, TeaBuildElement> namedElements = new TreeMap<>();
private final Map<Integer, List<TeaBuildElement>> groupedElements = new TreeMap<>();
private final List<TeaBuildVisitor> visitors;
private final IEclipseContext context;
private static final String PROJECTS_KEY = "TeaBuildChain.projectsToBuild";
/**
* Public for Injection, use {@link #make(IEclipseContext, Collection)} to
* create instances manually.
*/
@Inject
public TeaBuildChain(IEclipseContext context, TaskingLog log,
@Named(PROJECTS_KEY) Collection<IProject> projectsToBuild,
@Service List<TeaBuildElementFactory> elementFactories,
@Service List<TeaDependencyWireFactory> dependencyFactories, @Service List<TeaBuildVisitor> visitors) {
this.visitors = visitors;
this.context = context;
// Step 1: create all elements, so that dependency calculation can
// access them
elementFactories.forEach(f -> ContextInjectionFactory.invoke(f, Execute.class, context, null));
for (IProject prj : projectsToBuild) {
List<TeaBuildElement> elements = elementFactories.stream().map(f -> f.createElements(this, prj))
.filter(Objects::nonNull).flatMap(l -> l.stream()).filter(Objects::nonNull)
.collect(Collectors.toList());
if (elements.isEmpty()) {
// nobody knows how to handle this... create a dummy element to
// allow dependencies.
elements.add(new TeaUnhandledElement(prj.getName()));
}
elements.forEach(e -> namedElements.put(e.getName(), e));
}
// Step 2: calculate dependencies of each element
dependencyFactories.forEach(f -> ContextInjectionFactory.invoke(f, Execute.class, context, null));
dependencyFactories.forEach(f -> f.createWires(this));
// Step 3: sort elements by build order into groups. lazily triggers
// calculation of order groups (getBuildOrder()).
namedElements.values().forEach(e -> groupedElements.computeIfAbsent(e.getBuildOrder(), ArrayList::new).add(e));
}
/**
* Creates a {@link TeaBuildChain} that is capable of building the given
* {@link IProject}s.
*
* @param ctx
* the {@link IEclipseContext} used by TEA (by the TEA Task
* calling this method).
* @param projectsToBuild
* the projects to build. May be anything from a hand-selected
* list of projects to all projects in the workspace.
* @return a {@link TeaBuildChain} that contains all elements required to
* build the given projects.
*/
public static TeaBuildChain make(IEclipseContext ctx, Collection<IProject> projectsToBuild) {
IEclipseContext context = ctx.createChild("TeaBuildChain");
context.set(PROJECTS_KEY, projectsToBuild);
TeaBuildChain chain = ContextInjectionFactory.make(TeaBuildChain.class, context);
context.set(TeaBuildChain.class, chain);
return chain;
}
/**
* Retrieve the {@link TeaBuildElement} for the given name. This can be used
* to lookup specific elements e.g. for dependency wiring.
*
* @see TeaDependencyWireFactory
*
* @param name
* the name of the element.
* @return the corresponding {@link TeaBuildElement} or <code>null</code> if
* it does not exist.
*/
public TeaBuildElement getElementFor(String name) {
return namedElements.get(name);
}
/**
* @return all {@link TeaBuildElement}s that are currently participating in
* the {@link TeaBuildChain}.
*/
public Collection<TeaBuildElement> getAllElements() {
return namedElements.values();
}
/**
* @return the amount of work, equaling the amount of participating
* {@link TeaBuildElement}s.
*/
public int getTotalProgress() {
return groupedElements.size();
}
/**
* Execute the {@link TeaBuildChain}, building all {@link TeaBuildElement}s
* participating.
*
* @param tracker
* a {@link TaskProgressTracker} to update progress on. Also used
* for cancellation check. Total progress can be queried using
* {@link #getTotalProgress()} for tracker setup.
* @param failureThreshold
* the amount of failed build elements before stopping any
* further processing. The failure threshold might be exceeded
* due to batch processing of elements that have no dependencies
* to each other.
* @return {@link IStatus} describing the overall build status.
*/
public IStatus execute(TaskProgressTracker tracker, long failureThreshold) {
MultiStatus ms = new MultiStatus(Activator.PLUGIN_ID, IStatus.OK, "Build", null);
context.set(IStatus.class, ms);
for (TeaBuildVisitor visitor : visitors) {
ContextInjectionFactory.invoke(visitor, Execute.class, context, null);
}
long failures = 0;
// elements are in order already
for (Map.Entry<Integer, List<TeaBuildElement>> entry : groupedElements.entrySet()) {
if (tracker.isCanceled()) {
throw new OperationCanceledException();
}
tracker.setTaskName("Processing Group " + entry.getKey());
for (TeaBuildVisitor visitor : visitors) {
for (TeaBuildElement e : entry.getValue()) {
if (getVisitPolicyFor(e) == VisitPolicy.ABORT_IF_PREVIOUS_ERROR) {
if (ms.getSeverity() > IStatus.WARNING) {
ms.add(new Status(IStatus.CANCEL, Activator.PLUGIN_ID,
"Abort prior to executing " + e.getName() + " due to previous error"));
return ms;
}
}
}
Map<TeaBuildElement, IStatus> results = Optional.of(visitor.visit(entry.getValue()))
.orElse(Collections.emptyMap());
for (Entry<TeaBuildElement, IStatus> result : results.entrySet()) {
IStatus status = result.getValue();
if (status.getSeverity() > IStatus.WARNING) {
TeaBuildElement element = result.getKey();
switch (getFailurePolicyFor(element)) {
case ABORT_IMMEDIATE:
ms.add(status);
return ms;
case IGNORE:
ms.add(new Status(IStatus.WARNING, Activator.PLUGIN_ID,
"Ignored failure in element " + element.getName()));
break;
case USE_THRESHOLD:
ms.add(status);
if (failures++ >= failureThreshold) {
return ms;
}
}
}
}
}
tracker.worked(1);
}
return ms;
}
/**
* Determines how to handle a failure to build the given
* {@link TeaBuildElement}.
*
* @param element
* the element to query
* @return a {@link FailurePolicy} describing the action to take on failure.
*/
private FailurePolicy getFailurePolicyFor(TeaBuildElement element) {
TeaElementFailurePolicy a = element.getClass().getAnnotation(TeaElementFailurePolicy.class);
if (a == null) {
return FailurePolicy.USE_THRESHOLD;
}
return a.value();
}
/**
* Determines how to handle a failure prior to building the given
* {@link TeaBuildElement}.
*
* @param element
* the element to query
* @return a {@link VisitPolicy} describing the action to take on failure.
*/
private VisitPolicy getVisitPolicyFor(TeaBuildElement element) {
TeaElementVisitPolicy a = element.getClass().getAnnotation(TeaElementVisitPolicy.class);
if (a == null) {
return VisitPolicy.USE_THRESHOLD;
}
return a.value();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(getClass().getSimpleName()).append(":\n");
groupedElements.forEach(
(g, e) -> e.forEach(x -> builder.append("\t").append(g).append(": ").append(x.getName()).append("\n")));
return builder.toString();
}
}