/*******************************************************************************
* Copyright (c) 2015 GoPivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.validation;
import static org.springframework.ide.eclipse.boot.quickfix.GeneratorComposition.NO_RESOLUTIONS;
import static org.springframework.ide.eclipse.boot.validation.BootMarkerUtils.getProject;
import org.eclipse.core.internal.resources.ResourceException;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.ui.IMarkerResolution;
import org.eclipse.ui.IMarkerResolutionGenerator2;
import org.springframework.ide.eclipse.boot.core.BootActivator;
import org.springframework.ide.eclipse.boot.core.ISpringBootProject;
import org.springframework.ide.eclipse.boot.core.MavenCoordinates;
import org.springframework.ide.eclipse.boot.core.SpringBootCore;
import org.springframework.ide.eclipse.boot.quickfix.MarkerResolutionRegistry;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.core.model.IModelElement;
import org.springframework.ide.eclipse.core.model.validation.IValidationContext;
import org.springframework.ide.eclipse.core.model.validation.ValidationProblem;
import org.springframework.ide.eclipse.core.model.validation.ValidationProblemAttribute;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
/**
* Validation rule that checks
*
* if: found @ConfigurationProperties annotation
* then: spring-boot-configuration-processor jar is on the project's classpath
*
* Provides a quickfix to add spring-boot-configuration-processor to dependencies in
* pom-based project.
*
* @author Kris De Volder
*/
public class MissingConfigurationProcessorRule extends BootValidationRule {
private static final String PROBLEM_ID = "MISSING_CONFIGURATION_PROCESSOR";
private static final MavenCoordinates DEP_CONFIGURATION_PROCESSOR =
new MavenCoordinates("org.springframework.boot", "spring-boot-configuration-processor", null);
private static final IMarkerResolutionGenerator2 QUICK_FIX = new IMarkerResolutionGenerator2() {
@Override
public IMarkerResolution[] getResolutions(IMarker marker) {
try {
final ISpringBootProject project = SpringBootCore.create(getProject(marker));
return new IMarkerResolution[] {
new IMarkerResolution() {
@Override
public String getLabel() {
return "Add spring-boot-configuration-processor to pom.xml";
}
@Override
public void run(IMarker marker) {
try {
project.addMavenDependency(DEP_CONFIGURATION_PROCESSOR, true, true);
project.updateProjectConfiguration(); //needed to actually enable APT, m2e does not
// automatically trigger this if a dependency gets added
} catch (Exception e) {
BootActivator.log(e);
}
}
}
};
} catch (Exception e) {
return NO_RESOLUTIONS;
}
}
@Override
public boolean hasResolutions(IMarker marker) {
try {
IProject project = getProject(marker);
if (project.hasNature(SpringBootCore.M2E_NATURE)) {
return true;
}
} catch (Exception e) {
BootActivator.log(e);
}
return false;
}
};
static {
MarkerResolutionRegistry.DEFAULT_INSTANCE.register(PROBLEM_ID, QUICK_FIX);
}
/**
* Classpath matcher that checks classpath to determine if this rule applies
*/
private static final ClasspathMatcher CLASSPATH_MATCHER = new ClasspathMatcher(false) {
@Override
protected boolean doMatch(IClasspathEntry[] classpath) {
for (IClasspathEntry e : classpath) {
if (isJarNameContaining(e, "spring-boot-configuration-processor")) {
//The rules is already satisfied so doesn't need to be checked
return false;
}
}
return true;
}
};
public static class ValidationVisitor {
private SpringBootValidationContext context;
private SpringCompilationUnit cu;
public ValidationVisitor(SpringBootValidationContext context, SpringCompilationUnit cu) {
this.context = context;
this.cu = cu;
}
public void visit(ICompilationUnit compilationUnit, IProgressMonitor mon) throws Exception {
if (compilationUnit.exists()) {
IType[] types = compilationUnit.getAllTypes();
mon.beginTask(compilationUnit.getElementName(), types.length);
try {
for (IType t : types) {
visit(t, new SubProgressMonitor(mon, 1));
}
} finally {
mon.done();
}
}
}
private void visit(IType t, SubProgressMonitor mon) throws Exception {
IMethod[] methods = t.getMethods();
mon.beginTask(t.getElementName(), 1+methods.length);
try {
IAnnotation annot = getAnnotation(t);
if (annot!=null && annot.exists()) {
visit(annot);
mon.worked(1);
}
for (IMethod m : methods) {
visit(m, new SubProgressMonitor(mon, 1));
}
} finally {
mon.done();
}
}
private IAnnotation getAnnotation(IType t) {
try {
IAnnotation[] all = t.getAnnotations();
if (all!=null) {
for (IAnnotation a : all) {
String name = a.getElementName();
//name could be fully qualified or simple, so check for both
if ("org.springframework.boot.context.properties.ConfigurationProperties".equals(name)
|| "ConfigurationProperties".equals(name)
) {
return a;
}
}
}
} catch (JavaModelException e) {
BootActivator.log(e);
}
return null;
}
private void visit(IAnnotation annot) throws Exception {
warn("When using @ConfigurationProperties it is recommended to add 'spring-boot-configuration-processor' "
+ "to your classpath to generate configuration metadata", annot.getNameRange());
}
private void visit(IMethod m, SubProgressMonitor mon) throws Exception {
mon.beginTask(m.getElementName(), 1);
try {
IAnnotation annot = m.getAnnotation("ConfigurationProperties");
if (annot!=null && annot.exists()) {
visit(annot);
mon.worked(1);
}
} finally {
mon.done();
}
}
void warn(String msg, ISourceRange location) {
if (location!=null) {
context.warning(cu, PROBLEM_ID, msg,
new ValidationProblemAttribute(IMarker.CHAR_START, location.getOffset()),
new ValidationProblemAttribute(IMarker.CHAR_END, location.getOffset()+location.getLength())
);
// context.addProblems(new ValidationProblem(PROBLEM_ID, IMarker.SEVERITY_WARNING,
// msg, cu.getElementResource(), location));
}
}
}
@Override
public boolean supports(IModelElement element, IValidationContext context) {
return element instanceof SpringCompilationUnit;
}
@Override
public void validate(SpringCompilationUnit cu, SpringBootValidationContext context, IProgressMonitor mon) {
try{
if (CLASSPATH_MATCHER.match(cu.getClasspath())) {
ValidationVisitor visitor = new ValidationVisitor(context, cu);
visitor.visit(cu.getCompilationUnit(), mon);
}
} catch (Exception e) {
if (ExceptionUtil.getMessage(e).contains("File not found")) {
//Somewhat expected see [aer] https://www.pivotaltracker.com/story/show/133998741
Log.warn(e);
} else {
Log.log(e);
}
}
}
}