/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.tool.checkstyle;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* Verify that all classes annotated with {@code org.xwiki.component.annotation.Component} are in sync with the
* {@code META-INF/components.txt} file.
*
* @version $Id: 153f021f41284671b8a54e39c6134e1cdd8a6907 $
* @since 8.1M1
*/
public class ComponentAnnotationCheck extends AbstractCheck
{
private static final String COMPONENTS_TXT_LOCATION = "META-INF/components.txt";
private static final String COMPONENT_CLASS_NAME = "org.xwiki.component.annotation.Component";
private static final String SINGLETON_CLASS_NAME = "javax.inject.Singleton";
private static final String INSTANTIATION_STRATEGY_CLASS_NAME =
"org.xwiki.component.annotation.InstantiationStrategy";
private String packageName;
private String className;
private List<String> registeredComponentNames;
private URL componentsDeclarationLocation;
private Class<? extends Annotation> componentAnnotationClass;
private Class<? extends Annotation> singletonAnnotationClass;
private Class<? extends Annotation> instantiationStrategyAnnotationClass;
@Override
public int[] getDefaultTokens()
{
return new int[]{
TokenTypes.PACKAGE_DEF, TokenTypes.CLASS_DEF
};
}
@Override
public void init()
{
super.init();
// Important: We use reflection to load the annotation classes since otherwise we would need to
// depend on the xwiki-commons-component-api module and this would create a dependency cycle in
// xwiki-commons-core, preventing the build of the Commons reactor project.
this.componentAnnotationClass = loadAnnotationClass(COMPONENT_CLASS_NAME);
this.instantiationStrategyAnnotationClass = loadAnnotationClass(INSTANTIATION_STRATEGY_CLASS_NAME);
this.singletonAnnotationClass = loadAnnotationClass(SINGLETON_CLASS_NAME);
}
@Override
public void visitToken(DetailAST ast)
{
if (this.componentAnnotationClass == null || this.instantiationStrategyAnnotationClass == null
|| this.singletonAnnotationClass == null)
{
return;
}
switch (ast.getType()) {
case TokenTypes.PACKAGE_DEF:
// Save the package
FullIdent ident = FullIdent.createFullIdent(ast.getLastChild().getPreviousSibling());
this.packageName = ident.getText();
return;
case TokenTypes.CLASS_DEF:
// Only handle root classes (and not nested classes). This would be more complex to handle and we do not
// put nested components in general
if (ast.getParent() == null) {
this.className = ast.findFirstToken(TokenTypes.IDENT).getText();
} else {
return;
}
}
// Check 1:
// A - Verify that if there's at least one @Component annotation and "staticRegistration = true" then there
// needs to be a components.txt file
// Check 2:
// A- Verify that Classes annotated with @Component are defined in components.txt (unless the
// "staticRegistration = false" annotation parameter is specified)
// B- Verify that if the "staticRegistration = false" annotation parameter is specified then the Component
// must not be declared in components.txt
// Check 3:
// A- Verify that either @Singleton or @InstantiationStrategy are used on any class annotated with @Component
Class<?> componentClass;
try {
componentClass = getClassLoader().loadClass(getFullClassName());
} catch (ClassNotFoundException e) {
log(ast.getLineNo(), ast.getColumnNo(), String.format(
"Failed to load class in package [%s]: [%s]", this.packageName, getThrowableString(e)));
return;
}
Annotation componentAnnotation = componentClass.getAnnotation(this.componentAnnotationClass);
if (componentAnnotation != null) {
// Parse the components.txt if not already parsed for the current maven module
if (this.registeredComponentNames == null) {
this.registeredComponentNames = parseComponentsTxtFile(ast);
}
if (!isStaticRegistration(componentAnnotation)) {
// This is check 2-B
if (this.registeredComponentNames.contains(getFullClassName())) {
log(ast.getLineNo(), ast.getColumnNo(), String.format(
"Component [%s] is declared in [%s] but it is also declared with a "
+ "\"staticRegistration\" parameter with a [false] value, e.g. "
+ "\"@Component(staticRegistration = false\". You need to fix that!",
getFullClassName(), this.componentsDeclarationLocation));
}
} else {
// This is check 2-A
if (!this.registeredComponentNames.contains(getFullClassName())) {
log(ast.getLineNo(), ast.getColumnNo(), String.format(
"Component [%s] is not declared in [%s]! Consider adding it or if it is normal use "
+ "the \"staticRegistration\" parameter as in "
+ "\"@Component(staticRegistration = false)\"",
getFullClassName(), this.componentsDeclarationLocation));
}
}
// This is check 3-A
Annotation instantiationStrategyAnnotation =
componentClass.getAnnotation(this.instantiationStrategyAnnotationClass);
Annotation singletonAnnotation = componentClass.getAnnotation(this.singletonAnnotationClass);
if (instantiationStrategyAnnotation == null && singletonAnnotation == null) {
log(ast.getLineNo(), ast.getColumnNo(), String.format(
"Component class [%s] must have either the [%s] or the [%s] annotation defined on it.",
getFullClassName(), SINGLETON_CLASS_NAME, INSTANTIATION_STRATEGY_CLASS_NAME));
}
}
}
private boolean isStaticRegistration(Annotation componentAnnotation)
{
boolean isStaticRegistration = true;
try {
isStaticRegistration =
(Boolean) componentAnnotation.getClass().getMethod("staticRegistration").invoke(componentAnnotation);
} catch (Exception e) {
log(1, 1, String.format("Failed to find out if Component annotation is statically registered or not! "
+ "Reason: [%s]", getThrowableString(e)));
}
return isStaticRegistration;
}
private List<String> parseComponentsTxtFile(DetailAST ast)
{
List<String> results = new ArrayList<>();
try {
Enumeration<URL> urls = getClassLoader().getResources(COMPONENTS_TXT_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// We find the right components.txt by checking that the URL is using a "file" scheme (maven points
// to the target directory). For dependencies the URL scheme will be "jar".
if (url.getProtocol().equals("file")) {
this.componentsDeclarationLocation = url;
break;
}
}
} catch (Exception e) {
log(1, 1, String.format("Failed to locate [%s]. Error [%s]", COMPONENTS_TXT_LOCATION,
getThrowableString(e)));
return Collections.emptyList();
}
try (BufferedReader in = new BufferedReader(
new InputStreamReader(this.componentsDeclarationLocation.openStream())))
{
String inputLine;
while ((inputLine = in.readLine()) != null) {
// Make sure we don't include empty lines
if (inputLine.trim().length() > 0) {
try {
String[] chunks = inputLine.split(":");
if (chunks.length > 1) {
results.add(chunks[1]);
} else {
results.add(chunks[0]);
}
} catch (Exception e) {
log(ast.getLineNo(), ast.getColumnNo(), String.format(
"Invalid format [%s] in [%s]", inputLine, this.componentsDeclarationLocation));
}
}
}
} catch (Exception e) {
// Since this current method is called only if there's at least one @Component annotation with static
// registration, report an error if the components.txt file cannot be found
// Ths is check 1-A
log(ast.getLineNo(), ast.getColumnNo(), String.format(
"There is no [%s] file and thus Component [%s] isn't declared! Consider "
+ "adding a components.txt file or if it is normal use the \"staticRegistration\" parameter as "
+ "in \"@Component(staticRegistration = false)\"", this.componentsDeclarationLocation,
getFullClassName()));
}
return results;
}
private String getFullClassName()
{
return String.format("%s.%s", this.packageName, this.className);
}
private String getThrowableString(Throwable t)
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
t.printStackTrace(pw);
return sw.getBuffer().toString();
}
private Class<? extends Annotation> loadAnnotationClass(String annotationClassString)
{
Class<? extends Annotation> annotationClass;
try {
annotationClass = getClassLoader().loadClass(annotationClassString).asSubclass(Annotation.class);
} catch (Exception e) {
// This means that we're in a module that doesn't have a dependency on xwiki-commons-component-api (where
// Component and InstantiationStrategy annotations are located) and thus this module cannot define
// components... So we just ignore those modules.
annotationClass = null;
}
return annotationClass;
}
}