/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library 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 library 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.
*/
package com.liferay.source.formatter.checkstyle.checks;
import com.liferay.portal.kernel.util.CharPool;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Tuple;
import com.liferay.source.formatter.checks.util.JavaSourceUtil;
import com.liferay.source.formatter.util.SourceFormatterUtil;
import com.liferay.source.formatter.util.ThreadSafeClassLibrary;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FileContents;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.AbstractBaseJavaEntity;
import com.thoughtworks.qdox.model.Annotation;
import com.thoughtworks.qdox.model.DefaultDocletTagFactory;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaPackage;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.model.Type;
import com.thoughtworks.qdox.model.annotation.AnnotationValue;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author Hugo Huijser
*/
public class MissingOverrideCheck extends AbstractCheck {
public static final String MSG_MISSING_OVERRIDE = "override.missing";
@Override
public int[] getDefaultTokens() {
return new int[] {TokenTypes.PACKAGE_DEF};
}
@Override
public void visitToken(DetailAST detailAST) {
FileContents fileContents = getFileContents();
String fileName = StringUtil.replace(
fileContents.getFileName(), '\\', '/');
JavaDocBuilder javaDocBuilder = null;
try {
javaDocBuilder = _getJavaDocBuilder(fileName);
}
catch (Exception e) {
return;
}
if (javaDocBuilder == null) {
return;
}
JavaClass javaClass = javaDocBuilder.getClassByName(
_getPackagePath(detailAST) + "." + _getClassName(fileName));
List<Tuple> ancestorJavaClassTuples = _addAncestorJavaClassTuples(
javaClass, fileName, javaDocBuilder, new ArrayList<Tuple>());
for (JavaMethod javaMethod : javaClass.getMethods()) {
if (javaMethod.getLineNumber() == 0) {
continue;
}
if (!_hasAnnotation(javaMethod, "Override") &&
_isOverrideMethod(
javaClass, javaMethod, ancestorJavaClassTuples)) {
log(javaMethod.getLineNumber(), MSG_MISSING_OVERRIDE);
}
}
}
private List<Tuple> _addAncestorJavaClassTuples(
JavaClass javaClass, String fileName, JavaDocBuilder javaDocBuilder,
List<Tuple> ancestorJavaClassTuples) {
JavaClass superJavaClass = _fixJavaClass(
javaClass.getSuperJavaClass(), javaClass.getPackageName(), fileName,
javaDocBuilder);
if (superJavaClass != null) {
ancestorJavaClassTuples.add(new Tuple(superJavaClass));
ancestorJavaClassTuples = _addAncestorJavaClassTuples(
superJavaClass, null, javaDocBuilder, ancestorJavaClassTuples);
}
Type[] implementz = javaClass.getImplements();
for (Type implement : implementz) {
Type[] actualTypeArguments = implement.getActualTypeArguments();
JavaClass implementedInterface = _fixJavaClass(
implement.getJavaClass(), javaClass.getPackageName(), fileName,
javaDocBuilder);
if (actualTypeArguments == null) {
ancestorJavaClassTuples.add(new Tuple(implementedInterface));
}
else {
ancestorJavaClassTuples.add(
new Tuple(implementedInterface, actualTypeArguments));
}
ancestorJavaClassTuples = _addAncestorJavaClassTuples(
implementedInterface, null, javaDocBuilder,
ancestorJavaClassTuples);
}
return ancestorJavaClassTuples;
}
private JavaClass _fixJavaClass(
JavaClass javaClass, String packageName, String fileName,
JavaDocBuilder javaDocBuilder) {
// The methods getImplements and getSuperJavaClass in
// com.thoughtworks.qdox.model.JavaClass incorrectly return an empty
// class when the implemented or superclass are in the same package.
// This method corrects that.
if ((javaClass == null) || (fileName == null)) {
return javaClass;
}
String fullyQualifiedName = javaClass.getFullyQualifiedName();
if (fullyQualifiedName.contains(StringPool.PERIOD)) {
return javaClass;
}
int pos = fileName.lastIndexOf("/");
File file = new File(
fileName.substring(0, pos + 1) + fullyQualifiedName + ".java");
try {
javaDocBuilder.addSource(file);
}
catch (Exception e) {
return javaClass;
}
return javaDocBuilder.getClassByName(
packageName + "." + fullyQualifiedName);
}
private String _getClassName(String fileName) {
int pos = fileName.lastIndexOf('/');
return fileName.substring(pos + 1, fileName.length() - 5);
}
private JavaDocBuilder _getJavaDocBuilder(String fileName)
throws Exception {
if (_javaDocBuilder != null) {
return _javaDocBuilder;
}
JavaDocBuilder javaDocBuilder = new JavaDocBuilder(
new DefaultDocletTagFactory(), new ThreadSafeClassLibrary());
String absolutePath = JavaSourceUtil.getAbsolutePath(fileName);
while (true) {
int x = absolutePath.lastIndexOf("/");
if (x == -1) {
return null;
}
absolutePath = absolutePath.substring(0, x);
File file = new File(absolutePath + "/portal-impl");
if (file.exists()) {
break;
}
}
List<String> fileNames = SourceFormatterUtil.scanForFiles(
absolutePath + "/", _EXCLUDES, new String[] {"**/*.java"}, true);
for (String curFileName : fileNames) {
curFileName = StringUtil.replace(
curFileName, CharPool.BACK_SLASH, CharPool.SLASH);
try {
javaDocBuilder.addSource(
new File(JavaSourceUtil.getAbsolutePath(curFileName)));
}
catch (Exception e) {
}
}
_javaDocBuilder = javaDocBuilder;
return _javaDocBuilder;
}
private String _getPackagePath(DetailAST packageDefAST) {
DetailAST dotAST = packageDefAST.findFirstToken(TokenTypes.DOT);
FullIdent fullIdent = FullIdent.createFullIdent(dotAST);
return fullIdent.getText();
}
private boolean _hasAnnotation(
AbstractBaseJavaEntity abstractBaseJavaEntity, String annotationName) {
Annotation[] annotations = abstractBaseJavaEntity.getAnnotations();
if (annotations == null) {
return false;
}
for (int i = 0; i < annotations.length; i++) {
Type type = annotations[i].getType();
JavaClass javaClass = type.getJavaClass();
if (annotationName.equals(javaClass.getName())) {
return true;
}
}
return false;
}
private boolean _isOverrideMethod(
JavaClass javaClass, JavaMethod javaMethod,
Collection<Tuple> ancestorJavaClassTuples) {
if (javaMethod.isConstructor() || javaMethod.isPrivate() ||
javaMethod.isStatic() ||
_overridesHigherJavaAPIVersion(javaMethod)) {
return false;
}
String methodName = javaMethod.getName();
JavaParameter[] javaParameters = javaMethod.getParameters();
Type[] types = new Type[javaParameters.length];
for (int i = 0; i < javaParameters.length; i++) {
types[i] = javaParameters[i].getType();
}
// Check for matching method in each ancestor
for (Tuple ancestorJavaClassTuple : ancestorJavaClassTuples) {
JavaClass ancestorJavaClass =
(JavaClass)ancestorJavaClassTuple.getObject(0);
JavaMethod ancestorJavaMethod = null;
String ancestorJavaClassName =
ancestorJavaClass.getFullyQualifiedName();
if ((ancestorJavaClassTuple.getSize() == 1) ||
(ancestorJavaClassName.equals("java.util.Map") &&
methodName.equals("get"))) {
ancestorJavaMethod = ancestorJavaClass.getMethodBySignature(
methodName, types);
}
else {
// LPS-35613
Type[] ancestorActualTypeArguments =
(Type[])ancestorJavaClassTuple.getObject(1);
Type[] genericTypes = new Type[types.length];
for (int i = 0; i < types.length; i++) {
Type type = types[i];
String typeValue = type.getValue();
boolean useGenericType = false;
for (int j = 0; j < ancestorActualTypeArguments.length;
j++) {
if (typeValue.equals(
ancestorActualTypeArguments[j].getValue())) {
useGenericType = true;
break;
}
}
if (useGenericType) {
genericTypes[i] = new Type("java.lang.Object");
}
else {
genericTypes[i] = type;
}
}
ancestorJavaMethod = ancestorJavaClass.getMethodBySignature(
methodName, genericTypes);
}
if (ancestorJavaMethod == null) {
continue;
}
boolean samePackage = false;
JavaPackage ancestorJavaPackage = ancestorJavaClass.getPackage();
if (ancestorJavaPackage != null) {
samePackage = ancestorJavaPackage.equals(
javaClass.getPackage());
}
// Check if the method is in scope
if (samePackage) {
return !ancestorJavaMethod.isPrivate();
}
if (ancestorJavaMethod.isProtected() ||
ancestorJavaMethod.isPublic()) {
return true;
}
else {
return false;
}
}
return false;
}
private boolean _overridesHigherJavaAPIVersion(JavaMethod javaMethod) {
Annotation[] annotations = javaMethod.getAnnotations();
if (annotations == null) {
return false;
}
for (Annotation annotation : annotations) {
Type type = annotation.getType();
JavaClass javaClass = type.getJavaClass();
String javaClassName = javaClass.getFullyQualifiedName();
if (javaClassName.equals(SinceJava.class.getName())) {
AnnotationValue annotationValue = annotation.getProperty(
"value");
double sinceJava = GetterUtil.getDouble(
annotationValue.getParameterValue());
if (sinceJava > _LOWEST_SUPPORTED_JAVA_VERSION) {
return true;
}
}
}
return false;
}
private static final String[] _EXCLUDES = new String[] {
"**/.git/**", "**/.gradle/**", "**/bin/**", "**/build/**",
"**/classes/**", "**/node_modules/**", "**/npm-shrinkwrap.json",
"**/test-classes/**", "**/test-coverage/**", "**/test-results/**",
"**/tmp/**"
};
private static final double _LOWEST_SUPPORTED_JAVA_VERSION = 1.7;
private JavaDocBuilder _javaDocBuilder;
}