blob: 54863f98c394acf9960450b230328501269462fb [file] [log] [blame]
/*
* Copyright (c) 2014-2017 Eike Stepper (Loehne, Germany) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.oomph.version.digest;
import org.eclipse.oomph.util.StringUtil;
import org.eclipse.oomph.version.IBuildState;
import org.eclipse.oomph.version.IElement;
import org.eclipse.oomph.version.IRelease;
import org.eclipse.oomph.version.IReleaseManager;
import org.eclipse.oomph.version.VersionUtil;
import org.eclipse.oomph.version.VersionValidator;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.IModel;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.osgi.framework.Version;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
/**
* @author Eike Stepper
*/
public class DigestValidator extends VersionValidator
{
private static final Map<IRelease, ReleaseDigest> RELEASE_DIGESTS = new WeakHashMap<>();
public DigestValidator()
{
}
@Override
public String getVersion()
{
return Activator.getVersion();
}
@Override
public void updateBuildState(IBuildState buildState, IRelease release, IProject project, IResourceDelta delta, IModel componentModel,
IProgressMonitor monitor) throws Exception
{
DigestValidatorState validatorState = (DigestValidatorState)buildState.getValidatorState(componentModel);
IPath releasePath = release.getFile().getFullPath();
ReleaseDigest releaseDigest = getReleaseDigest(releasePath, release, monitor);
// Check whether the release digest to use is still the one that has been used the for the last build.
long timeStamp = releaseDigest.getTimeStamp();
if (timeStamp != buildState.getValidatorTimeStamp(componentModel))
{
// Trigger full validation if the release digest to use is different from the one used for the last build
delta = null;
// Avoid triggering full builds after this (full) build
buildState.setValidatorTimeStamp(componentModel, timeStamp);
}
beforeValidation(validatorState, componentModel);
if (validatorState == null || delta == null)
{
if (VersionUtil.DEBUG)
{
System.out.println("Digest: Full validation..."); //$NON-NLS-1$
}
buildState.setValidatorState(componentModel, null);
validatorState = validateFull(project, null, componentModel, monitor);
}
else
{
if (VersionUtil.DEBUG)
{
System.out.println("Digest: Incremental validation..."); //$NON-NLS-1$
}
validatorState = validateDelta(delta, validatorState, componentModel, monitor);
}
afterValidation(validatorState);
if (validatorState == null)
{
throw new IllegalStateException(Messages.DigestValidator_NoValidationState_exception);
}
byte[] validatorDigest = validatorState.getDigest();
if (VersionUtil.DEBUG)
{
System.out.println("DIGEST = " + formatDigest(validatorDigest)); //$NON-NLS-1$
}
byte[] releasedProjectDigest = releaseDigest.get(getName(project, componentModel));
boolean changedSinceRelease = false;
if (releasedProjectDigest != null)
{
if (VersionUtil.DEBUG)
{
System.out.println("RELEASE = " + formatDigest(releasedProjectDigest)); //$NON-NLS-1$
}
changedSinceRelease = !MessageDigest.isEqual(validatorDigest, releasedProjectDigest);
}
buildState.setChangedSinceRelease(changedSinceRelease);
buildState.setValidatorState(componentModel, validatorState);
}
public DigestValidatorState validateFull(IResource resource, DigestValidatorState parentState, IModel componentModel, IProgressMonitor monitor)
throws Exception
{
if (resource.getType() != IResource.PROJECT && (!isConsidered(resource) || !resource.exists()))
{
return null;
}
if (VersionUtil.DEBUG)
{
System.out.println("Digest: " + resource.getFullPath()); //$NON-NLS-1$
}
DigestValidatorState result = new DigestValidatorState();
result.setName(getName(resource, componentModel));
result.setParent(parentState);
if (resource instanceof IContainer)
{
IContainer container = (IContainer)resource;
List<DigestValidatorState> memberStates = new ArrayList<>();
for (IResource member : container.members())
{
DigestValidatorState memberState = validateFull(member, result, componentModel, monitor);
if (memberState != null)
{
memberStates.add(memberState);
}
}
byte[] digest = getFolderDigest(memberStates);
if (VersionUtil.DEBUG)
{
System.out.println("Considered: " + container.getFullPath() + " --> " + formatDigest(digest)); //$NON-NLS-1$ //$NON-NLS-2$
}
result.setDigest(digest);
result.setChildren(memberStates.toArray(new DigestValidatorState[memberStates.size()]));
}
else
{
IFile file = (IFile)resource;
byte[] digest = getFileDigest(file);
if (VersionUtil.DEBUG)
{
System.out.println("Considered: " + file.getFullPath() + " --> " + formatDigest(digest)); //$NON-NLS-1$ //$NON-NLS-2$
}
result.setDigest(digest);
}
return result;
}
public DigestValidatorState validateDelta(IResourceDelta delta, DigestValidatorState validatorState, IModel componentModel, IProgressMonitor monitor)
throws Exception
{
IResource resource = delta.getResource();
if (!resource.exists() || resource.getType() != IResource.PROJECT && !isConsidered(resource))
{
return null;
}
DigestValidatorState result = validatorState;
switch (delta.getKind())
{
case IResourceDelta.ADDED:
result = new DigestValidatorState();
result.setName(getName(resource, componentModel));
//$FALL-THROUGH$
case IResourceDelta.CHANGED:
if (resource instanceof IContainer)
{
Set<DigestValidatorState> memberStates = new HashSet<>();
for (IResourceDelta memberDelta : delta.getAffectedChildren())
{
IResource memberResource = memberDelta.getResource();
DigestValidatorState memberState = validatorState != null ? validatorState.getChild(memberResource.getName()) : null;
DigestValidatorState newMemberState = validateDelta(memberDelta, memberState, componentModel, monitor);
if (newMemberState != null)
{
newMemberState.setParent(result);
memberStates.add(newMemberState);
}
}
if (validatorState != null)
{
IContainer container = (IContainer)resource;
for (DigestValidatorState oldChild : validatorState.getChildren())
{
IResource member = container.findMember(oldChild.getName());
if (member != null)
{
memberStates.add(oldChild);
}
}
}
byte[] digest = getFolderDigest(memberStates);
result.setDigest(digest);
result.setChildren(memberStates.toArray(new DigestValidatorState[memberStates.size()]));
// VersionBuilder.trace(" " + delta.getFullPath() + " --> " + TestResourceChangeListener.getKind(delta) +
// " " + TestResourceChangeListener.getFlags(delta));
}
else
{
boolean changed = result == validatorState;
if (changed && (delta.getFlags() & IResourceDelta.CONTENT) == 0)
{
return validatorState;
}
IFile file = (IFile)resource;
byte[] digest = getFileDigest(file);
result.setDigest(digest);
// VersionBuilder.trace(" " + delta.getFullPath() + " --> " + TestResourceChangeListener.getKind(delta) +
// " " + TestResourceChangeListener.getFlags(delta));
}
break;
case IResourceDelta.REMOVED:
result = null;
}
return result;
}
protected boolean isConsidered(IResource resource)
{
return !resource.isDerived();
}
protected void beforeValidation(DigestValidatorState validatorState, IModel componentModel) throws Exception
{
}
protected void afterValidation(DigestValidatorState validatorState) throws Exception
{
}
private ReleaseDigest getReleaseDigest(IPath releasePath, IRelease release, IProgressMonitor monitor)
throws IOException, CoreException, ClassNotFoundException
{
IFile file = getDigestFile(releasePath);
long localTimeStamp = file.getLocalTimeStamp();
ReleaseDigest releaseDigest = RELEASE_DIGESTS.get(release);
if (releaseDigest != null)
{
// Check whether digest file has changed on disk.
if (localTimeStamp != releaseDigest.getTimeStamp())
{
// Digest file has changed. Reload it further down.
releaseDigest = null;
}
}
if (releaseDigest == null)
{
if (file.exists())
{
releaseDigest = readDigestFile(file);
// Check whether the digest file on disk (still) matches the release spec
if (!MessageDigest.isEqual(releaseDigest.getReleaseSpecDigest(), release.getDigest()))
{
releaseDigest = null;
}
}
if (releaseDigest == null)
{
releaseDigest = createReleaseDigest(release, file, null, monitor);
}
releaseDigest.setTimeStamp(localTimeStamp);
RELEASE_DIGESTS.put(release, releaseDigest);
}
return releaseDigest;
}
private byte[] getFolderDigest(Collection<DigestValidatorState> states) throws Exception
{
List<DigestValidatorState> list = new ArrayList<>(states);
Collections.sort(list);
MessageDigest digest = MessageDigest.getInstance("SHA"); //$NON-NLS-1$
for (DigestValidatorState state : list)
{
byte[] bytes = state.getDigest();
if (bytes != null)
{
digest.update(state.getName().getBytes());
digest.update(bytes);
}
}
return digest.digest();
}
private byte[] getFileDigest(IFile file) throws Exception
{
return VersionUtil.getSHA1(file);
}
private String formatDigest(byte[] digest)
{
StringBuilder builder = new StringBuilder();
for (byte b : digest)
{
if (builder.length() != 0)
{
builder.append(", "); //$NON-NLS-1$
}
builder.append("(byte)"); //$NON-NLS-1$
builder.append(b);
}
return builder.toString();
}
private ReleaseDigest readDigestFile(IFile file) throws IOException, CoreException, ClassNotFoundException
{
ObjectInputStream stream = null;
try
{
stream = new ObjectInputStream(file.getContents());
return (ReleaseDigest)stream.readObject();
}
finally
{
if (stream != null)
{
try
{
stream.close();
}
catch (Exception ex)
{
Activator.log(ex);
}
}
}
}
private void writeReleaseDigest(ReleaseDigest releaseDigest, IFile target, IProgressMonitor monitor) throws IOException, CoreException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(releaseDigest);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
if (target.exists())
{
int i = 1;
for (;;)
{
try
{
target.move(target.getFullPath().addFileExtension("bak" + i), true, monitor); //$NON-NLS-1$
break;
}
catch (Exception ex)
{
++i;
}
}
}
target.create(bais, true, monitor);
monitor.worked(1);
}
private void addWarning(List<String> warnings, String msg)
{
Activator.log(new Status(IStatus.WARNING, Activator.PLUGIN_ID, msg));
if (warnings != null)
{
warnings.add(msg);
}
}
private String getName(IResource resource, IModel model)
{
String name = resource.getName();
if (resource.getType() == IResource.PROJECT && VersionUtil.getType(model) == IElement.Type.PRODUCT)
{
name += "/" + model.getUnderlyingResource().getProjectRelativePath(); //$NON-NLS-1$
}
return name;
}
public ReleaseDigest createReleaseDigest(IRelease release, IFile target, List<String> warnings, IProgressMonitor monitor) throws CoreException
{
monitor.beginTask(null, release.getSize() + 1);
try
{
ReleaseDigest releaseDigest = new ReleaseDigest(release.getDigest());
for (Entry<IElement, IElement> entry : release.getElements().entrySet())
{
String name = entry.getKey().getName();
monitor.subTask(name);
try
{
try
{
IElement element = entry.getValue();
if (element.getName().endsWith(".source")) //$NON-NLS-1$
{
continue;
}
IModel componentModel = IReleaseManager.INSTANCE.getComponentModel(element.trimVersion());
if (componentModel == null)
{
addWarning(warnings, NLS.bind(Messages.DigestValidator_ComponentNotFound_message, name));
continue;
}
IResource resource = componentModel.getUnderlyingResource();
if (resource == null)
{
String type = componentModel instanceof IPluginModelBase ? Messages.DigestValidator_Plugin_message_part
: Messages.DigestValidator_Feature_message_part;
addWarning(warnings, NLS.bind(Messages.DigestValidator_NotInWorkspace_message, name, type));
continue;
}
Version version = VersionUtil.getComponentVersion(componentModel);
if (!element.getVersion().equals(version))
{
String type = componentModel instanceof IPluginModelBase ? Messages.DigestValidator_Plugin_message_part
: Messages.DigestValidator_Feature_message_part;
addWarning(warnings, NLS.bind(Messages.DigestValidator_VersionIsNot_message, new Object[] { name, type, element.getVersion() }));
}
IProject project = resource.getProject();
beforeValidation(null, componentModel);
DigestValidatorState state = validateFull(project, null, componentModel, monitor);
afterValidation(state);
releaseDigest.put(state.getName(), state.getDigest());
}
finally
{
monitor.worked(1);
}
}
catch (Exception ex)
{
addWarning(warnings, name + ": " + Activator.getStatus(ex).getMessage()); //$NON-NLS-1$
}
}
writeReleaseDigest(releaseDigest, target, monitor);
return releaseDigest;
}
catch (CoreException ex)
{
throw ex;
}
catch (Exception ex)
{
throw new CoreException(Activator.getStatus(ex));
}
finally
{
monitor.done();
}
}
public static IFile getDigestFile(IPath releasePath)
{
return VersionUtil.getFile(releasePath, "digest"); //$NON-NLS-1$
}
/**
* @author Eike Stepper
*/
public static class BuildModel extends DigestValidator
{
@SuppressWarnings("restriction")
private static final String[] ICON_IDS = { org.eclipse.pde.internal.core.iproduct.ILauncherInfo.LINUX_ICON,
org.eclipse.pde.internal.core.iproduct.ILauncherInfo.MACOSX_ICON, org.eclipse.pde.internal.core.iproduct.ILauncherInfo.WIN32_16_HIGH,
org.eclipse.pde.internal.core.iproduct.ILauncherInfo.WIN32_32_LOW, org.eclipse.pde.internal.core.iproduct.ILauncherInfo.WIN32_32_HIGH,
org.eclipse.pde.internal.core.iproduct.ILauncherInfo.WIN32_48_LOW, org.eclipse.pde.internal.core.iproduct.ILauncherInfo.WIN32_48_HIGH,
org.eclipse.pde.internal.core.iproduct.ILauncherInfo.WIN32_256_HIGH };
private Set<String> considered = new HashSet<>();
public BuildModel()
{
}
@SuppressWarnings("restriction")
@Override
protected void beforeValidation(DigestValidatorState validatorState, IModel componentModel) throws Exception
{
considered.clear();
considered.add(""); //$NON-NLS-1$
if (VersionUtil.getType(componentModel) == IElement.Type.PRODUCT)
{
IPath projectRelativePath = componentModel.getUnderlyingResource().getProjectRelativePath();
consider(projectRelativePath);
consider(projectRelativePath.removeFileExtension().addFileExtension("p2.inf")); //$NON-NLS-1$
org.eclipse.pde.internal.core.iproduct.IProductModel productModel = (org.eclipse.pde.internal.core.iproduct.IProductModel)componentModel;
org.eclipse.pde.internal.core.iproduct.IProduct product = productModel.getProduct();
org.eclipse.pde.internal.core.iproduct.ILauncherInfo launcherInfo = product.getLauncherInfo();
for (String iconID : ICON_IDS)
{
String iconPath = launcherInfo.getIconPath(iconID);
if (!StringUtil.isEmpty(iconPath))
{
consider(new Path(iconPath));
}
}
}
else
{
IBuild build = VersionUtil.getBuild(componentModel);
if (build != null)
{
IBuildEntry binIncludes = build.getEntry(IBuildEntry.BIN_INCLUDES);
if (binIncludes != null)
{
for (String binInclude : binIncludes.getTokens())
{
IBuildEntry sources = build.getEntry("source." + binInclude); //$NON-NLS-1$
if (sources != null)
{
for (String source : sources.getTokens())
{
consider(new Path(source));
}
}
else
{
consider(new Path(binInclude));
}
}
}
}
}
}
@Override
protected void afterValidation(DigestValidatorState validatorState) throws Exception
{
considered.clear();
}
@Override
protected boolean isConsidered(IResource resource)
{
IPath path = resource.getProjectRelativePath();
while (!path.isEmpty())
{
if (considered.contains(path.toString()))
{
return true;
}
path = path.removeLastSegments(1);
}
return false;
}
private void consider(IPath path)
{
for (IPath basePath = path.removeTrailingSeparator(); basePath.segmentCount() > 0; basePath = basePath.removeLastSegments(1))
{
if (!considered.add(basePath.toString()))
{
break;
}
}
}
}
}