| /******************************************************************************* |
| * Copyright (c) 2013, 2016 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.releng.internal.tools.pomversion; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Stack; |
| import java.util.jar.JarFile; |
| |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| import org.eclipse.core.filebuffers.FileBuffers; |
| import org.eclipse.core.filebuffers.ITextFileBuffer; |
| import org.eclipse.core.filebuffers.ITextFileBufferManager; |
| import org.eclipse.core.filebuffers.LocationKind; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceChangeListener; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.resources.IResourceDeltaVisitor; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.osgi.util.ManifestElement; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.releng.tools.RelEngPlugin; |
| import org.eclipse.ui.texteditor.MarkerUtilities; |
| import org.osgi.framework.BundleException; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.Version; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| |
| /** |
| * Validates the content of the pom.xml. Currently the only check is that the |
| * version specified in pom.xml matches the bundle version. |
| * |
| */ |
| public class PomVersionErrorReporter implements IResourceChangeListener, IEclipsePreferences.IPreferenceChangeListener { |
| |
| class PomResourceDeltaVisitor implements IResourceDeltaVisitor { |
| |
| @Override |
| public boolean visit(IResourceDelta delta) { |
| if (delta != null) { |
| IResource resource = delta.getResource(); |
| switch(resource.getType()) { |
| case IResource.PROJECT: { |
| if(delta.getKind() == IResourceDelta.REMOVED) { |
| return false; |
| } |
| IProject project = (IProject) resource; |
| try { |
| if(project.isAccessible() && (project.getDescription().hasNature("org.eclipse.pde.PluginNature") || project.getDescription().hasNature("org.eclipse.pde.FeatureNature"))) { //$NON-NLS-1$ //$NON-NLS-2$ |
| if((delta.getFlags() & IResourceDelta.OPEN) > 0) { |
| validate(project); |
| return false; |
| } |
| return true; |
| } |
| } |
| catch(CoreException ce) { |
| RelEngPlugin.log(ce); |
| } |
| return false; |
| } |
| case IResource.ROOT: |
| case IResource.FOLDER: { |
| return true; |
| } |
| case IResource.FILE: { |
| switch(delta.getKind()) { |
| case IResourceDelta.REMOVED: { |
| //if manifest or feature removed, clean up markers |
| if(resource.getProjectRelativePath().equals(FEATURE_PATH) || |
| resource.getProjectRelativePath().equals(MANIFEST_PATH)) { |
| IProject p = resource.getProject(); |
| if(p.isAccessible()) { |
| cleanMarkers(p); |
| } |
| } |
| break; |
| } |
| case IResourceDelta.ADDED: { |
| //if the POM, manifest or feature.xml has been added scan them |
| if(resource.getProjectRelativePath().equals(FEATURE_PATH) || |
| resource.getProjectRelativePath().equals(MANIFEST_PATH) || |
| resource.getProjectRelativePath().equals(POM_PATH)) { |
| validate(resource.getProject()); |
| } |
| break; |
| } |
| case IResourceDelta.CHANGED: { |
| //if the content has changed clean + scan |
| if((delta.getFlags() & IResourceDelta.CONTENT) > 0) { |
| if(resource.getProjectRelativePath().equals(FEATURE_PATH) || |
| resource.getProjectRelativePath().equals(MANIFEST_PATH) || |
| resource.getProjectRelativePath().equals(POM_PATH)) { |
| validate(resource.getProject()); |
| } |
| } |
| break; |
| } |
| default: { |
| break; |
| } |
| } |
| return false; |
| } |
| } |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * XML parsing handler to check the POM version infos |
| */ |
| class PomVersionHandler extends DefaultHandler { |
| private Version version; |
| private Stack<String> elements = new Stack<>(); |
| private boolean checkVersion = false; |
| private boolean isFeatureProject = false; |
| private Locator locator; |
| IFile pom = null; |
| String severity = null; |
| |
| public PomVersionHandler(IFile file, Version bundleVersion, String pref) { |
| this(file, bundleVersion, pref, false); |
| } |
| |
| public PomVersionHandler(IFile file, Version version, String pref, boolean isFeatureProject) { |
| pom = file; |
| severity = pref; |
| this.version = version; |
| this.isFeatureProject = isFeatureProject; |
| } |
| |
| @Override |
| public void setDocumentLocator(Locator locator) { |
| this.locator = locator; |
| } |
| |
| @Override |
| public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { |
| if (ELEMENT_VERSION.equals(qName)) { |
| if (!elements.isEmpty() && ELEMENT_PROJECT.equals(elements.peek())) { |
| checkVersion = true; |
| } |
| } |
| elements.push(qName); |
| } |
| |
| @Override |
| public void endElement(String uri, String localName, String qName) throws SAXException { |
| elements.pop(); |
| } |
| |
| @Override |
| public void characters(char[] ch, int start, int length) throws SAXException { |
| if (checkVersion) { |
| checkVersion = false; |
| |
| try { |
| // Remove the snapshot suffix |
| String versionString = new String(ch, start, length); |
| String origVer = versionString; |
| int index = versionString.indexOf(SNAPSHOT_SUFFIX); |
| if (index >= 0) { |
| versionString = versionString.substring(0, index); |
| } |
| |
| // Create corrected version (no qualifiers, add back snapshot suffix) |
| Version bundleVersion2 = new Version(version.getMajor(), version.getMinor(), version.getMicro()); |
| String correctedVersion = bundleVersion2.toString(); |
| if (index >= 0) { |
| correctedVersion = correctedVersion.concat(SNAPSHOT_SUFFIX); |
| } |
| |
| // Check if the pom version is a valid OSGi version |
| Version pomVersion = null; |
| try { |
| pomVersion = Version.parseVersion(versionString); |
| } catch (IllegalArgumentException e){ |
| // Need to create a document to calculate the markers charstart and charend |
| IDocument doc = createDocument(pom); |
| int lineOffset = doc.getLineOffset(locator.getLineNumber() - 1); // locator lines start at 1 |
| int linLength = doc.getLineLength(locator.getLineNumber() - 1); |
| String str = doc.get(lineOffset, linLength); |
| index = str.indexOf(origVer); |
| int charStart = lineOffset + index; |
| int charEnd = charStart + origVer.length(); |
| |
| String message = isFeatureProject ? Messages.PomVersionErrorReporter_pom_version_error_marker_message_feature : Messages.PomVersionErrorReporter_pom_version_error_marker_message; |
| reportMarker(NLS.bind(message, versionString, bundleVersion2.toString()), |
| locator.getLineNumber(), |
| charStart, |
| charEnd, |
| correctedVersion, |
| pom, |
| severity); |
| } |
| |
| if (pomVersion == null){ |
| return; |
| } |
| |
| // Compare the versions |
| Version pomVersion2 = new Version(pomVersion.getMajor(), pomVersion.getMinor(), pomVersion.getMicro()); |
| if (!bundleVersion2.equals(pomVersion2)) { |
| // Need to create a document to calculate the markers charstart and charend |
| IDocument doc = createDocument(pom); |
| int lineOffset = doc.getLineOffset(locator.getLineNumber() - 1); // locator lines start at 1 |
| int linLength = doc.getLineLength(locator.getLineNumber() - 1); |
| String str = doc.get(lineOffset, linLength); |
| index = str.indexOf(origVer); |
| int charStart = lineOffset + index; |
| int charEnd = charStart + origVer.length(); |
| String message = isFeatureProject ? Messages.PomVersionErrorReporter_pom_version_error_marker_message_feature : Messages.PomVersionErrorReporter_pom_version_error_marker_message; |
| reportMarker(NLS.bind(message, pomVersion2.toString(), bundleVersion2.toString()), |
| locator.getLineNumber(), |
| charStart, |
| charEnd, |
| correctedVersion, |
| pom, |
| severity); |
| } |
| } catch (IllegalArgumentException e) { |
| // If the manifest version is broken, let PDE report the problem |
| } catch (BadLocationException e) { |
| RelEngPlugin.log(e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * XML parsing handler to check the feature.xml version |
| */ |
| class FeatureVersionHandler extends DefaultHandler { |
| private String featureVersion; |
| |
| public FeatureVersionHandler() { |
| } |
| |
| @Override |
| public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { |
| // The version is on the root element, check for the attribute then throw exception to exit early |
| featureVersion = attributes.getValue("version"); //$NON-NLS-1$ |
| throw new OperationCanceledException(); |
| } |
| |
| /** |
| * Returns the string version value found in the feature.xml or <code>null</code> |
| * |
| * @return string version from feature.xml or <code>null</code> |
| */ |
| public String getVersion() { |
| return featureVersion; |
| } |
| |
| } |
| |
| /** |
| * Project relative path to the pom.xml file. |
| */ |
| public static final IPath POM_PATH = new Path("pom.xml"); //$NON-NLS-1$ |
| |
| /** |
| * Project relative path to the manifest file. |
| */ |
| public static final IPath MANIFEST_PATH = new Path(JarFile.MANIFEST_NAME); |
| |
| /** |
| * Version of the PomVersionErrorReporter. Needs to be incremented when the version check algorithm changes |
| * in a way that requires re-validation of the whole workspace. |
| */ |
| public static final int VERSION= 1; |
| |
| /** |
| * Project relative path to the feature.xml file. |
| */ |
| public static final IPath FEATURE_PATH = new Path("feature.xml"); //$NON-NLS-1$ |
| |
| private static final String ELEMENT_PROJECT = "project"; //$NON-NLS-1$ |
| private static final String ELEMENT_VERSION = "version"; //$NON-NLS-1$ |
| private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT"; //$NON-NLS-1$ |
| |
| |
| /** |
| * Clean up all markers |
| * |
| * @param project |
| */ |
| void cleanMarkers(IResource resource) { |
| try { |
| resource.deleteMarkers(IPomVersionConstants.PROBLEM_MARKER_TYPE, false, IResource.DEPTH_INFINITE); |
| } |
| catch(CoreException e) { |
| RelEngPlugin.log(e); |
| } |
| } |
| |
| /** |
| * Validates the manifest or feature version against the version in the <code>pom.xml</code> file |
| * |
| * @param project |
| * @param severity |
| */ |
| public void validate(IProject project) { |
| if(project == null || !project.isAccessible()) { |
| return; |
| } |
| // Clean up existing markers |
| cleanMarkers(project); |
| |
| String severity = RelEngPlugin.getPlugin().getPreferenceStore().getString(IPomVersionConstants.POM_VERSION_ERROR_LEVEL); |
| if (IPomVersionConstants.VALUE_IGNORE.equals(severity)) { |
| return; |
| } |
| IFile pom = project.getFile(POM_PATH); |
| if(!pom.exists()) { |
| return; |
| } |
| |
| IFile manifest = project.getFile(MANIFEST_PATH); |
| if(manifest.exists()) { |
| // Get the manifest version |
| Version bundleVersion = Version.emptyVersion; |
| try { |
| Map<String, String> headers = new HashMap<>(); |
| ManifestElement.parseBundleManifest(manifest.getContents(), headers); |
| String ver = headers.get(Constants.BUNDLE_VERSION); |
| if(ver == null) { |
| return; |
| } |
| bundleVersion = Version.parseVersion(ver); |
| } catch (IOException e) { |
| // Ignored, if there is a problem with the manifest, don't create a marker |
| return; |
| } catch (CoreException e){ |
| // Ignored, if there is a problem with the manifest, don't create a marker |
| return; |
| } catch (BundleException e) { |
| // Ignored, if there is a problem with the manifest, don't create a marker |
| return; |
| } catch (IllegalArgumentException e){ |
| // Ignored, if there is a problem with the manifest, don't create a marker |
| return; |
| } |
| // Compare it to the POM file version |
| try { |
| SAXParserFactory parserFactory = SAXParserFactory.newInstance(); |
| SAXParser parser = parserFactory.newSAXParser(); |
| PomVersionHandler handler = new PomVersionHandler(pom, bundleVersion, severity); |
| parser.parse(pom.getContents(), handler); |
| } catch (Exception e) { |
| // Ignored, if there is a problem with the POM file don't create a marker |
| return; |
| } |
| |
| } else { |
| IFile feature = project.getFile(FEATURE_PATH); |
| if (feature.exists()){ |
| try { |
| // Get the feature version |
| Version featureVersion = Version.emptyVersion; |
| SAXParserFactory parserFactory = SAXParserFactory.newInstance(); |
| SAXParser parser = parserFactory.newSAXParser(); |
| FeatureVersionHandler handler = new FeatureVersionHandler(); |
| try { |
| parser.parse(feature.getContents(), handler); |
| } catch (OperationCanceledException e){ |
| // Do nothing, used to avoid parsing the entire file |
| } |
| |
| String version = handler.getVersion(); |
| if (version == null){ |
| // Ignored, if there is a problem with the feature, don't create a marker |
| return; |
| } |
| featureVersion = Version.parseVersion(version); |
| |
| // Compare it to the POM file version |
| PomVersionHandler pomHandler = new PomVersionHandler(pom, featureVersion, severity, true); |
| parser.parse(pom.getContents(), pomHandler); |
| } catch (Exception e) { |
| // Ignored, if there is a problem with the POM file don't create a marker |
| return; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates a new POM version problem marker with the given attributes |
| * @param message the message for the marker |
| * @param lineNumber the line number of the problem |
| * @param charStart the starting character offset |
| * @param charEnd the ending character offset |
| * @param correctedVersion the correct version to be inserted |
| * @param pom the handle to the POM file |
| * @param severity the severity of the marker to create |
| */ |
| void reportMarker(String message, int lineNumber, int charStart, int charEnd, String correctedVersion, IFile pom, String severity) { |
| try { |
| HashMap<String, Object> attributes = new HashMap<>(); |
| attributes.put(IMarker.MESSAGE, message); |
| if (severity.equals(IPomVersionConstants.VALUE_WARNING)){ |
| attributes.put(IMarker.SEVERITY, new Integer(IMarker.SEVERITY_WARNING)); |
| } else { |
| attributes.put(IMarker.SEVERITY, new Integer(IMarker.SEVERITY_ERROR)); |
| } |
| if (lineNumber == -1) { |
| lineNumber = 1; |
| } |
| attributes.put(IMarker.LINE_NUMBER, new Integer(lineNumber)); |
| attributes.put(IMarker.CHAR_START, new Integer(charStart)); |
| attributes.put(IMarker.CHAR_END, new Integer(charEnd)); |
| attributes.put(IPomVersionConstants.POM_CORRECT_VERSION, correctedVersion); |
| MarkerUtilities.createMarker(pom, attributes, IPomVersionConstants.PROBLEM_MARKER_TYPE); |
| } catch (CoreException e){ |
| RelEngPlugin.log(e); |
| } |
| } |
| |
| /** |
| * Creates a new {@link IDocument} for the given {@link IFile}. <code>null</code> |
| * is returned if the {@link IFile} does not exist or the {@link ITextFileBufferManager} |
| * cannot be acquired or there was an exception trying to create the {@link IDocument}. |
| * |
| * @param file |
| * @return a new {@link IDocument} or <code>null</code> |
| */ |
| protected IDocument createDocument(IFile file) { |
| if (!file.exists()) { |
| return null; |
| } |
| ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager(); |
| if (manager == null) { |
| return null; |
| } |
| try { |
| manager.connect(file.getFullPath(), LocationKind.NORMALIZE, null); |
| ITextFileBuffer textBuf = manager.getTextFileBuffer(file.getFullPath(), LocationKind.NORMALIZE); |
| IDocument document = textBuf.getDocument(); |
| manager.disconnect(file.getFullPath(), LocationKind.NORMALIZE, null); |
| return document; |
| } catch (CoreException e) { |
| RelEngPlugin.log(e); |
| } |
| return null; |
| } |
| |
| @Override |
| public void resourceChanged(IResourceChangeEvent event) { |
| IResourceDelta delta = event.getDelta(); |
| if(delta != null) { |
| final PomResourceDeltaVisitor visitor = new PomResourceDeltaVisitor(); |
| try { |
| delta.accept(visitor); |
| } catch (CoreException e) { |
| RelEngPlugin.log(e); |
| } |
| } |
| } |
| |
| @Override |
| public void preferenceChange(PreferenceChangeEvent event) { |
| if(IPomVersionConstants.POM_VERSION_ERROR_LEVEL.equals(event.getKey())) { |
| final String newSeverity = (String) event.getNewValue(); |
| final Object oldSeverity= event.getOldValue(); |
| if(newSeverity != null) { |
| if(IPomVersionConstants.VALUE_IGNORE.equals(newSeverity)) { |
| //we turned it off |
| ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); |
| } else if(oldSeverity == null || IPomVersionConstants.VALUE_IGNORE.equals(oldSeverity)) { |
| // we turned it on |
| ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_BUILD); |
| } |
| validateWorkspace(); |
| } |
| } |
| } |
| |
| public void validateWorkspace() { |
| IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); |
| IProject[] projects = root.getProjects(); |
| for (int i = 0; i < projects.length; i++) { |
| validate(projects[i]); |
| } |
| RelEngPlugin.getPlugin().getPreferenceStore().setValue(IPomVersionConstants.WORKSPACE_VALIDATED, VERSION); |
| } |
| } |