Bug 575348: [Ltk-Project] Add extensible builder extracted from
Tex/Wikitext

Change-Id: I4ff65967af1a4dc7b1d4f8716efb603fe5546d25
diff --git a/ltk/org.eclipse.statet.ltk.core/META-INF/MANIFEST.MF b/ltk/org.eclipse.statet.ltk.core/META-INF/MANIFEST.MF
index 521b666..d468e3f 100644
--- a/ltk/org.eclipse.statet.ltk.core/META-INF/MANIFEST.MF
+++ b/ltk/org.eclipse.statet.ltk.core/META-INF/MANIFEST.MF
@@ -41,4 +41,6 @@
  org.eclipse.statet.ltk.model.core.element,
  org.eclipse.statet.ltk.model.core.impl,
  org.eclipse.statet.ltk.model.core.util,
+ org.eclipse.statet.ltk.project.core,
+ org.eclipse.statet.ltk.project.core.builder,
  org.eclipse.statet.ltk.refactoring.core
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/LtkProject.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/LtkProject.java
new file mode 100644
index 0000000..5da7c67
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/LtkProject.java
@@ -0,0 +1,31 @@
+/*=============================================================================#
+ # Copyright (c) 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ltk.project.core;
+
+import org.eclipse.core.resources.IProject;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+import org.eclipse.statet.ecommons.preferences.core.PreferenceAccess;
+
+
+@NonNullByDefault
+public interface LtkProject extends PreferenceAccess {
+	
+	
+	IProject getProject();
+	
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectBuildParticipant.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectBuildParticipant.java
new file mode 100644
index 0000000..7de7f24
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectBuildParticipant.java
@@ -0,0 +1,92 @@
+/*=============================================================================#
+ # Copyright (c) 2014, 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ltk.project.core.builder;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.SubMonitor;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+import org.eclipse.statet.ltk.model.core.element.WorkspaceSourceUnit;
+import org.eclipse.statet.ltk.project.core.LtkProject;
+
+
+@NonNullByDefault
+public class ProjectBuildParticipant<TProject extends LtkProject,
+		TSourceUnit extends WorkspaceSourceUnit> {
+	
+	
+	TProject ltkProject= nonNullLateInit();
+	
+	int buildType;
+	
+	boolean enabled;
+	
+	
+	public ProjectBuildParticipant() {
+	}
+	
+	
+	public final TProject getLtkProject() {
+		return this.ltkProject;
+	}
+	
+	public final int getBuildType() {
+		return this.buildType;
+	}
+	
+	public void init() {
+	}
+	
+	protected final void setEnabled(final boolean enabled) {
+		this.enabled= enabled;
+	}
+	
+	public final boolean isEnabled() {
+		return this.enabled;
+	}
+	
+	/**
+	 * @param file the file to clear
+	 * @throws CoreException
+	 */
+	public void clearSourceUnit(final IFile file) throws CoreException {
+	}
+	
+	/**
+	 * @param sourceUnit the added/changed source unit
+	 * @param monitor SubMonitor-recommended
+	 */
+	public void handleSourceUnitUpdated(final TSourceUnit sourceUnit,
+			final SubMonitor m) throws CoreException {
+		// update index
+	}
+	
+	/**
+	 * @param file the removed resource
+	 * @param monitor SubMonitor-recommended
+	 */
+	public void handleSourceUnitRemoved(final IFile file,
+			final SubMonitor m) throws CoreException {
+		// remove from index
+	}
+	
+	public void finish(final SubMonitor m) throws CoreException {
+	}
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectBuildTask.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectBuildTask.java
new file mode 100644
index 0000000..25d0eee
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectBuildTask.java
@@ -0,0 +1,342 @@
+/*=============================================================================#
+ # Copyright (c) 2014, 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ltk.project.core.builder;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.core.runtime.content.IContentDescription;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.osgi.util.NLS;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ltk.core.Ltk;
+import org.eclipse.statet.ltk.model.core.LtkModels;
+import org.eclipse.statet.ltk.model.core.SourceUnitManager;
+import org.eclipse.statet.ltk.model.core.element.SourceUnit;
+import org.eclipse.statet.ltk.model.core.element.WorkspaceSourceUnit;
+import org.eclipse.statet.ltk.project.core.LtkProject;
+
+
+@NonNullByDefault
+public abstract class ProjectBuildTask<TProject extends LtkProject,
+				TSourceUnit extends WorkspaceSourceUnit,
+				TParticipant extends ProjectBuildParticipant<TProject, TSourceUnit>>
+		extends ProjectTask<TProject, TSourceUnit, TParticipant>
+		implements IResourceVisitor, IResourceDeltaVisitor {
+	
+	
+	private final static class VirtualSourceUnit {
+		
+		private final IFile file;
+		
+		private final String modelTypeId;
+		
+		
+		public VirtualSourceUnit(final IFile file, final String modelTypeId) {
+			this.file= file;
+			this.modelTypeId= modelTypeId;
+		}
+		
+		
+		public IFile getResource() {
+			return this.file;
+		}
+		
+		public String getModelTypeId() {
+			return this.modelTypeId;
+		}
+		
+		
+		@Override
+		public int hashCode() {
+			return this.file.hashCode();
+		}
+		
+		@Override
+		public String toString() {
+			return this.file.toString();
+		}
+		
+	}
+	
+	
+	private final SourceUnitManager suManager= LtkModels.getSourceUnitManager();
+	
+	private final List<TSourceUnit> updatetSourceUnits;
+	private final List<VirtualSourceUnit> removedFiles;
+	
+	private SubMonitor visitProgress= nonNullLateInit();
+	
+	
+	public ProjectBuildTask(final ProjectTaskBuilder<TProject, TSourceUnit, TParticipant> builder) {
+		super(builder);
+		
+		this.updatetSourceUnits= new ArrayList<>();
+		this.removedFiles= new ArrayList<>();
+	}
+	
+	private void dispose(final SubMonitor m) {
+		m.setWorkRemaining(this.updatetSourceUnits.size());
+		for (final var unit : this.updatetSourceUnits) {
+			unit.disconnect(m.newChild(1));
+		}
+	}
+	
+	
+	public void build(final int kind,
+			final SubMonitor m) throws CoreException {
+		try {
+			m.beginTask(NLS.bind("Preparing TeX build for ''{0}''", getProject().getName()),
+					1 + 2 + 8 + 1 );
+			
+			final IResourceDelta delta;
+			switch (kind) {
+			case IncrementalProjectBuilder.AUTO_BUILD:
+			case IncrementalProjectBuilder.INCREMENTAL_BUILD:
+				delta= getBuilder().getDelta(getProject());
+				m.worked(1);
+				break;
+			default:
+				delta= null;
+			}
+			
+			if (m.isCanceled()) {
+				throw new CoreException(Status.CANCEL_STATUS);
+			}
+			m.setWorkRemaining(2 + 8 + 1);
+			
+			this.visitProgress= m.newChild(2);
+			if (delta != null) {
+				setBuildType(IncrementalProjectBuilder.INCREMENTAL_BUILD);
+				delta.accept(this);
+			}
+			else {
+				setBuildType(IncrementalProjectBuilder.FULL_BUILD);
+				getProject().accept(this);
+			}
+			this.visitProgress= null;
+			
+			if (m.isCanceled()) {
+				throw new CoreException(Status.CANCEL_STATUS);
+			}
+			processChanges(m.newChild(8, SubMonitor.SUPPRESS_NONE));
+		}
+		finally {
+			m.setWorkRemaining(1);
+			dispose(m.newChild(1));
+			
+			finish();
+		}
+	}
+	
+	
+	@Override
+	public boolean visit(final IResourceDelta delta) throws CoreException {
+		final IResource resource= delta.getResource();
+		if (resource.getType() == IResource.FILE) {
+			if (this.visitProgress.isCanceled()) {
+				throw new CoreException(Status.CANCEL_STATUS);
+			}
+			this.visitProgress.setWorkRemaining(100);
+			
+			try {
+				switch (delta.getKind()) {
+				case IResourceDelta.ADDED:
+				case IResourceDelta.CHANGED:
+					visitFileAdded((IFile)resource, delta, this.visitProgress.newChild(1));
+					break;
+				case IResourceDelta.REMOVED:
+					visitFileRemove((IFile)resource, delta, this.visitProgress.newChild(1));
+					break;
+				default:
+					break;
+				}
+			}
+			catch (final Exception e) {
+				this.status.add(new Status(IStatus.ERROR, getBuilderDefinition().getBundleId(),
+						NLS.bind("An error occurred when checking file ''{0}''", resource.getFullPath()),
+						e ));
+			}
+		}
+		
+		return true;
+	}
+	
+	@Override
+	public boolean visit(final IResource resource) throws CoreException {
+		if (resource.getType() == IResource.FILE) {
+			this.visitProgress.setWorkRemaining(100);
+			
+			visitFileAdded((IFile)resource, null, this.visitProgress.newChild(1));
+		}
+		return true;
+	}
+	
+	private void visitFileAdded(final IFile file, final @Nullable IResourceDelta delta,
+			final SubMonitor m) throws CoreException {
+		final IContentDescription contentDescription= file.getContentDescription();
+		if (contentDescription == null) {
+			return;
+		}
+		final IContentType contentType= contentDescription.getContentType();
+		if (contentType == null) {
+			return;
+		}
+		final var definition= getBuilderDefinition();
+		final int contentId= definition.checkSourceUnitContent(contentType);
+		if (contentId == 0) {
+			final SourceUnit unit= this.suManager.getSourceUnit(
+					Ltk.PERSISTENCE_CONTEXT, file, contentType, true, m );
+			if (definition.getSourceUnitType().isInstance(unit)) {
+				this.updatetSourceUnits.add((TSourceUnit)unit);
+			}
+			else {
+				if (unit != null) {
+					unit.disconnect(m);
+				}
+				clear(file, null);
+			}
+		}
+	}
+	
+	private void visitFileRemove(final IFile file, final IResourceDelta delta,
+			final SubMonitor m) throws CoreException {
+		// There is no contentDescription for removed files
+//		final IContentDescription contentDescription= file.getContentDescription();
+		
+//		if (contentType.isKindOf(LTX_CONTENT_TYPE)) {
+//			final ModelTypeDescriptor modelType= this.modelRegistry.getModelTypeForContentType(contentType.getId());
+//			final VirtualSourceUnit unit= new VirtualSourceUnit(file, (modelType != null) ? modelType.getId() : null);
+//			this.removedLtxFiles.add(unit);
+//			
+//			if ((delta != null && (delta.getFlags() & IResourceDelta.MOVED_TO) != 0)) {
+//				final IResource movedTo= file.getWorkspace().getRoot().findMember(delta.getMovedToPath());
+//				if (movedTo instanceof IFile) {
+//					final DocProject movedToProject= DocProject.getDocProject(movedTo.getProject());
+//					if (modelType == null
+//							|| movedToProject == null || movedToProject == getDocProject()
+//							|| !getDocProjectBuilder().hasBeenBuilt(movedToProject.getProject()) ) {
+//						clearLtx((IFile) movedTo, getParticipant(unit.getModelTypeId()));
+//					}
+//				}
+//			}
+//		}
+	}
+	
+	private void processChanges(final SubMonitor m) throws CoreException {
+		m.beginTask(String.format("Analyzing %1$s file(s) of '%2$s'",
+						getBuilderDefinition().getProjectTypeLabel(),
+						getProject().getName()),
+				10 + 10 );
+		
+		{	final SubMonitor m1= m.newChild(10);
+			int workRemaining= this.removedFiles.size() + this.updatetSourceUnits.size() * 5;
+			for (final VirtualSourceUnit unit : this.removedFiles) {
+				m1.setWorkRemaining(workRemaining--);
+				try {
+					final var participant= getParticipant(unit.getModelTypeId());
+					
+					if (participant != null) {
+						participant.handleSourceUnitRemoved(unit.getResource(), m1.newChild(1));
+					}
+				}
+				catch (final Exception e) {
+					this.status.add(new Status(IStatus.ERROR, getBuilderDefinition().getBundleId(),
+							NLS.bind("An error occurred when processing removed file ''{0}''.",
+									unit.getResource() ),
+							e ));
+				}
+				if (m1.isCanceled()) {
+					throw new CoreException(Status.CANCEL_STATUS);
+				}
+			}
+			
+			if (!this.updatetSourceUnits.isEmpty()) {
+				for (final var sourceUnit : this.updatetSourceUnits) {
+					m1.setWorkRemaining(workRemaining); workRemaining-= 5;
+					try {
+						final var participant= getParticipant(sourceUnit.getModelTypeId());
+						
+						clear((IFile)sourceUnit.getResource(), participant);
+						
+						reconcile(sourceUnit, m1.newChild(3));
+						
+						if (participant != null && participant.isEnabled()) {
+							participant.handleSourceUnitUpdated(sourceUnit, m1.newChild(2));
+						}
+					}
+					catch (final CancellationException e) {
+						throw new CoreException(Status.CANCEL_STATUS);
+					}
+					catch (final Exception e) {
+						this.status.add(new Status(IStatus.ERROR, getBuilderDefinition().getBundleId(),
+								NLS.bind("An error occurred when processing file ''{0}''.",
+										sourceUnit.getResource() ),
+								e ));
+					}
+					if (m1.isCanceled()) {
+						throw new CoreException(Status.CANCEL_STATUS);
+					}
+				}
+			}
+		}
+		{	final SubMonitor m1= m.newChild(10);
+			final var participants= getParticipants();
+			int workRemaining= participants.size();
+			for (final var participant : participants) {
+				m1.setWorkRemaining(workRemaining--);
+				if (participant.isEnabled()) {
+					try {
+						participant.finish(m1.newChild(1));
+					}
+					catch (final Exception e) {
+						this.status.add(new Status(IStatus.ERROR, getBuilderDefinition().getBundleId(),
+								String.format("An error occurred when processing %1$s file(s) in '%2$s'.",
+										getBuilderDefinition().getProjectTypeLabel(),
+										getProject().getName() ),
+								e ));
+					}
+				}
+			}
+		}
+	}
+	
+	protected abstract void reconcile(TSourceUnit sourceUnit, SubMonitor m);
+	
+	
+	private void clear(final IFile file, final @Nullable TParticipant partitipant)
+			throws CoreException {
+		if (partitipant != null) {
+			partitipant.clearSourceUnit(file);
+		}
+	}
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectCleanTask.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectCleanTask.java
new file mode 100644
index 0000000..77b23ab
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectCleanTask.java
@@ -0,0 +1,40 @@
+/*=============================================================================#
+ # Copyright (c) 2014, 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ltk.project.core.builder;
+
+import org.eclipse.core.runtime.SubMonitor;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+import org.eclipse.statet.ltk.model.core.element.WorkspaceSourceUnit;
+import org.eclipse.statet.ltk.project.core.LtkProject;
+
+
+@NonNullByDefault
+public class ProjectCleanTask<TProject extends LtkProject,
+				TSourceUnit extends WorkspaceSourceUnit,
+				TBuildParticipant extends ProjectBuildParticipant<TProject, TSourceUnit>>
+		extends ProjectTask<TProject, TSourceUnit, TBuildParticipant> {
+	
+	
+	public ProjectCleanTask(final ProjectTaskBuilder<TProject, TSourceUnit, TBuildParticipant> builder) {
+		super(builder);
+	}
+	
+	
+	public void clean(final SubMonitor m) {
+	}
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectTask.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectTask.java
new file mode 100644
index 0000000..fd1a082
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectTask.java
@@ -0,0 +1,127 @@
+/*=============================================================================#
+ # Copyright (c) 2014, 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ltk.project.core.builder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.MultiStatus;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ltk.model.core.LtkModels;
+import org.eclipse.statet.ltk.model.core.element.WorkspaceSourceUnit;
+import org.eclipse.statet.ltk.project.core.LtkProject;
+import org.eclipse.statet.ltk.project.core.builder.ProjectTaskBuilder.BuilderDefinition;
+
+
+@NonNullByDefault
+public abstract class ProjectTask<TProject extends LtkProject,
+				TSourceUnit extends WorkspaceSourceUnit,
+				TParticipant extends ProjectBuildParticipant<TProject, TSourceUnit>> {
+	
+	
+	@SuppressWarnings("rawtypes")
+	private static final ProjectBuildParticipant NO_PARTICIPANT= new ProjectBuildParticipant<>();
+	
+	
+	private final Map<String, TParticipant> participants= new HashMap<>();
+	
+	private final ProjectTaskBuilder<TProject, TSourceUnit, TParticipant> projectBuilder;
+	private final IProject project;
+	
+	private int buildType;
+	
+	protected final MultiStatus status;
+	
+	
+	public ProjectTask(final ProjectTaskBuilder<TProject, TSourceUnit, TParticipant> projectBuilder) {
+		this.projectBuilder= projectBuilder;
+		this.project= projectBuilder.getProject();
+		
+		this.status= new MultiStatus(getBuilderDefinition().getBundleId(), 0,
+				String.format("%1$s build status for '%2$s'",
+						getBuilderDefinition().getProjectTypeLabel(),
+						getProject().getName() ),
+				null );
+	}
+	
+	
+	protected final ProjectTaskBuilder<TProject, TSourceUnit, TParticipant> getBuilder() {
+		return this.projectBuilder;
+	}
+	
+	protected final BuilderDefinition<TProject, TSourceUnit, TParticipant> getBuilderDefinition() {
+		return this.projectBuilder.getBuilderDefinition();
+	}
+	
+	public final IProject getProject() {
+		return this.project;
+	}
+	
+	public void setBuildType(final int buildType) {
+		this.buildType= buildType;
+	}
+	
+	protected final Collection<TParticipant> getParticipants() {
+		final var values= this.participants.values();
+		final List<TParticipant> list= new ArrayList<>(values.size());
+		for (final var participant : values) {
+			if (participant != null) {
+				list.add(participant);
+			}
+		}
+		return list;
+	}
+	
+	protected final @Nullable TParticipant getParticipant(final String modelTypeId) {
+		if (modelTypeId == null) {
+			return null;
+		}
+		@Nullable TParticipant participant= this.participants.get(modelTypeId);
+		if (participant == null) {
+			participant= loadParticipant(modelTypeId);
+			this.participants.put(modelTypeId, participant);
+		}
+		return (participant != NO_PARTICIPANT) ? participant : null;
+	}
+	
+	@SuppressWarnings("unchecked")
+	private TParticipant loadParticipant(final String modelTypeId) {
+		final @Nullable TParticipant participant= (TParticipant)LtkModels.getModelAdapter(
+				modelTypeId, getBuilderDefinition().getParticipantType() );
+		if (participant == null) {
+			return (TParticipant)NO_PARTICIPANT;
+		}
+		participant.ltkProject= getBuilder().getLtkProject();
+		participant.buildType= this.buildType;
+		participant.enabled= false;
+		participant.init();
+		return participant;
+	}
+	
+	
+	protected void finish() {
+		if (!this.status.isOK()) {
+			getBuilder().log(this.status);
+		}
+	}
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectTaskBuilder.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectTaskBuilder.java
new file mode 100644
index 0000000..7fb7d27
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/project/core/builder/ProjectTaskBuilder.java
@@ -0,0 +1,209 @@
+/*=============================================================================#
+ # Copyright (c) 2014, 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ltk.project.core.builder;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
+
+import java.util.Map;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Plugin;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.osgi.util.NLS;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ltk.model.core.element.WorkspaceSourceUnit;
+import org.eclipse.statet.ltk.project.core.LtkProject;
+
+
+@NonNullByDefault
+public abstract class ProjectTaskBuilder<TProject extends LtkProject,
+				TSourceUnit extends WorkspaceSourceUnit,
+				TParticipant extends ProjectBuildParticipant<TProject, TSourceUnit>>
+		extends IncrementalProjectBuilder {
+	
+	
+	public static abstract class BuilderDefinition<TProject extends LtkProject,
+			TSourceUnit extends WorkspaceSourceUnit,
+			TBuildParticipant extends ProjectBuildParticipant<TProject, TSourceUnit>> {
+		
+		
+		private final String bundleId;
+		private final Plugin bundle;
+		
+		private final String projectNatureId;
+		private final String projectTypeLabel;
+		
+		private final Class<TSourceUnit> sourceUnitType;
+		
+		private final Class<TBuildParticipant> participantType;
+		
+		
+		public BuilderDefinition(final String bundleId, final Plugin bundle,
+				final String projectNatureId, final String projectLabel,
+				final Class<TSourceUnit> sourceUnitType,
+				final Class<TBuildParticipant> participantType) {
+			this.bundleId= bundleId;
+			this.bundle= bundle;
+			
+			this.projectNatureId= projectNatureId;
+			this.projectTypeLabel= projectLabel;
+			
+			this.sourceUnitType= sourceUnitType;
+			
+			this.participantType= participantType;
+		}
+		
+		
+		public String getBundleId() {
+			return this.bundleId;
+		}
+		
+		public Plugin getBundle() {
+			return this.bundle;
+		}
+		
+		
+		public String getProjectNatureId() {
+			return this.projectNatureId;
+		}
+		
+		public String getProjectTypeLabel() {
+			return this.projectTypeLabel;
+		}
+		
+		
+		public abstract int checkSourceUnitContent(final IContentType contentType);
+		
+		public Class<TSourceUnit> getSourceUnitType() {
+			return this.sourceUnitType;
+		}
+		
+		
+		public Class<TBuildParticipant> getParticipantType() {
+			return this.participantType;
+		}
+		
+	}
+	
+	
+	private final BuilderDefinition<TProject, TSourceUnit, TParticipant> definition;
+	
+	private TProject ltkProject= nonNullLateInit();
+	
+	
+	public ProjectTaskBuilder(final BuilderDefinition<TProject, TSourceUnit, TParticipant> definition) {
+		this.definition= definition;
+	}
+	
+	
+	public final BuilderDefinition<TProject, TSourceUnit, TParticipant> getBuilderDefinition() {
+		return this.definition;
+	}
+	
+	public TProject getLtkProject() {
+		return this.ltkProject;
+	}
+	
+	@Override
+	@SuppressWarnings("unchecked")
+	protected void startupOnInitialize() {
+		super.startupOnInitialize();
+		
+		try {
+			this.ltkProject= (TProject)getProject().getNature(this.definition.getProjectNatureId());
+		}
+		catch (final CoreException e) {
+			log(e.getStatus());
+		}
+	}
+	
+	@SuppressWarnings("unused")
+	private void check(final SubMonitor progress) throws CoreException {
+		if (this.ltkProject == null) {
+			throw new CoreException(new Status(IStatus.ERROR, this.definition.getBundleId(),
+					NLS.bind("{0} project nature is missing.",
+							this.definition.getProjectTypeLabel() )));
+		}
+	}
+	
+	
+	@Override
+	protected IProject @Nullable[] build(final int kind,
+			final @Nullable Map<String, @Nullable String> args,
+			final @Nullable IProgressMonitor monitor) throws CoreException {
+		final SubMonitor m= SubMonitor.convert(monitor, 1 + 20);
+		try {
+			check(m.newChild(1, SubMonitor.SUPPRESS_NONE));
+			
+			final var projectTask= createBuildTask();
+			projectTask.build(kind, m.newChild(20, SubMonitor.SUPPRESS_NONE));
+			
+			return null;
+		}
+		catch (final CoreException e) {
+			if (e.getStatus().getSeverity() == IStatus.CANCEL) {
+				throw new OperationCanceledException();
+			}
+			throw e;
+		}
+		finally {
+			m.done();
+		}
+	}
+	
+	@Override
+	protected void clean(final @Nullable IProgressMonitor monitor) throws CoreException {
+		final SubMonitor m= SubMonitor.convert(monitor, 1 + 20);
+		try {
+			check(m.newChild(1, SubMonitor.SUPPRESS_NONE));
+			
+			final var projectTask= createCleanTask();
+			projectTask.clean(m.newChild(20, SubMonitor.SUPPRESS_NONE));
+		}
+		catch (final CoreException e) {
+			if (e.getStatus().getSeverity() == IStatus.CANCEL) {
+				throw new OperationCanceledException();
+			}
+			throw e;
+		}
+		finally {
+			m.done();
+		}
+	}
+	
+	
+	protected abstract ProjectBuildTask<TProject, TSourceUnit, TParticipant> createBuildTask();
+	
+	protected abstract ProjectCleanTask<TProject, TSourceUnit, TParticipant> createCleanTask();
+	
+	
+	protected void log(final IStatus status) {
+		final var log= this.definition.getBundle().getLog();
+		if (log != null) {
+			log.log(status);
+		}
+	}
+	
+}