/*
* Copyright 2015 Lukas Krejci
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package org.revapi.java.spi;
import java.io.Reader;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeKind;
import org.revapi.AnalysisContext;
import org.revapi.Difference;
/**
* A basic implementation of the {@link Check} interface. This class easies the matching of the {@code visit*()}
* methods and their corresponding {@link #visitEnd()} by keeping track of the "depth" individual calls (see the
* recursive
* nature of the {@link org.revapi.java.spi.Check call order}).
*
* <p>This class also contains a couple of utility methods for checking the accessibility of elements, etc.
*
* @author Lukas Krejci
* @see #pushActive(JavaElement, JavaElement, Object...)
* @see #popIfActive()
* @since 0.1
*/
public abstract class CheckBase implements Check {
/**
* Checks whether both provided elements are (package) private. If one of them is null, the fact cannot be
* determined and therefore this method would return false.
*
* @param a first element
* @param b second element
* @return true if both elements are not null and are private or package private
*/
public boolean isBothPrivate(@Nullable JavaModelElement a, @Nullable JavaModelElement b) {
if (a == null || b == null) {
return false;
}
return !isAccessible(a) && !isAccessible(b);
}
/**
* Checks whether both provided elements are public or protected. If one at least one of them is null, the method
* returns false, because the accessibility cannot be truthfully detected in that case.
*
* @param a first element
* @param b second element
* @return true if both elements are not null and accessible (i.e. public or protected)
*/
public boolean isBothAccessible(@Nullable JavaModelElement a, @Nullable JavaModelElement b) {
if (a == null || b == null) {
return false;
}
return isAccessible(a) && isAccessible(b);
}
/**
* This method checks that the provided element and all its parent elements are accessible (public or protected).
* Additionally, if the provided element is a type, it must be in API or, if it is not a type, its nearest enclosing
* type must be in API.
*
* @param e the element to check
* @return true if the provided element is accessible and in API, false otherwise.
*/
public boolean isAccessible(@Nonnull JavaModelElement e) {
if (!isAccessibleByModifier(e.getDeclaringElement())) {
return false;
}
JavaModelElement parent = e.getParent();
if (e instanceof JavaTypeElement) {
return ((JavaTypeElement) e).isInAPI() && (parent == null || _isAccessible(parent));
} else {
assert parent != null;
return isAccessible(parent);
}
}
private boolean _isAccessible(@Nonnull JavaModelElement e) {
if (!isAccessibleByModifier(e.getDeclaringElement())) {
return false;
}
JavaModelElement parent = e.getParent();
return parent == null || _isAccessible(parent);
}
private boolean isAccessibleByModifier(Element e) {
return !isMissing(e) && (e.getModifiers().contains(Modifier.PUBLIC) ||
e.getModifiers().contains(Modifier.PROTECTED));
}
/**
* The element is deemed missing if its type kind ({@link javax.lang.model.type.TypeMirror#getKind()}) is
* {@link TypeKind#ERROR}.
*
* @param e the element
*
* @return true if the element is missing, false otherwise
*/
public boolean isMissing(@Nonnull Element e) {
return e.asType().getKind() == TypeKind.ERROR;
}
/**
* Represents the elements that have been {@link #pushActive(JavaElement, JavaElement, Object...) pushed} onto
* the active elements stack.
*
* @param <T> the type of elements
*/
protected static class ActiveElements<T extends JavaElement> {
public final T oldElement;
public final T newElement;
public final Object[] context;
private final int depth;
private ActiveElements(int depth, T oldElement, T newElement, Object... context) {
this.depth = depth;
this.oldElement = oldElement;
this.newElement = newElement;
this.context = context;
}
}
private TypeEnvironment oldTypeEnvironment;
private TypeEnvironment newTypeEnvironment;
private int depth;
private final Deque<ActiveElements<?>> activations = new ArrayDeque<>();
private AnalysisContext analysisContext;
@Nonnull
protected Difference createDifference(@Nonnull Code code, String[] attachments) {
return code.createDifference(getAnalysisContext().getLocale(), attachments);
}
@Nonnull
protected Difference createDifferenceWithExplicitParams(@Nonnull Code code,
String[] attachments,
String... params) {
return code.createDifferenceWithExplicitParams(getAnalysisContext().getLocale(), attachments, params);
}
@Nonnull
public TypeEnvironment getOldTypeEnvironment() {
return oldTypeEnvironment;
}
@Nonnull
public TypeEnvironment getNewTypeEnvironment() {
return newTypeEnvironment;
}
@Nonnull
public AnalysisContext getAnalysisContext() {
return analysisContext;
}
@Nullable
@Override
public String[] getConfigurationRootPaths() {
return null;
}
@Nullable
@Override
public Reader getJSONSchema(@Nonnull String configurationRootPath) {
return null;
}
@Override
public void initialize(@Nonnull AnalysisContext analysisContext) {
this.analysisContext = analysisContext;
}
@Override
public void setOldTypeEnvironment(@Nonnull TypeEnvironment env) {
oldTypeEnvironment = env;
}
@Override
public void setNewTypeEnvironment(@Nonnull TypeEnvironment env) {
newTypeEnvironment = env;
}
/**
* Please override the {@link #doEnd()} method instead.
*
* @see org.revapi.java.spi.Check#visitEnd()
*/
@Nullable
@Override
public final List<Difference> visitEnd() {
try {
return doEnd();
} finally {
//defensive pop if the doEnd "forgets" to do it.
//this is to prevent accidental retrieval of wrong data in the case the last active element was pushed
//by a "sibling" call which forgot to pop it. The current visit* + end combo would think it was active
//even if the visit call didn't push anything to the stack.
popIfActive();
depth--;
}
}
@Nullable
protected List<Difference> doEnd() {
return null;
}
/**
* Please override the
* {@link #doVisitClass(JavaTypeElement, JavaTypeElement)}
*
* @see Check#visitClass(JavaTypeElement, JavaTypeElement)
*/
@Override
public final void visitClass(@Nullable JavaTypeElement oldType, @Nullable JavaTypeElement newType) {
depth++;
doVisitClass(oldType, newType);
}
protected void doVisitClass(@Nullable JavaTypeElement oldType, @Nullable JavaTypeElement newType) {
}
/**
* Please override the
* {@link #doVisitMethod(JavaMethodElement, JavaMethodElement)}
* instead.
*
* @see Check#visitMethod(JavaMethodElement, JavaMethodElement)
*/
@Override
public final void visitMethod(@Nullable JavaMethodElement oldMethod, @Nullable JavaMethodElement newMethod) {
depth++;
doVisitMethod(oldMethod, newMethod);
}
protected void doVisitMethod(@Nullable JavaMethodElement oldMethod, @Nullable JavaMethodElement newMethod) {
}
@Override
public final void visitMethodParameter(@Nullable JavaMethodParameterElement oldParameter,
@Nullable JavaMethodParameterElement newParameter) {
depth++;
doVisitMethodParameter(oldParameter, newParameter);
}
@SuppressWarnings("UnusedParameters")
protected void doVisitMethodParameter(@Nullable JavaMethodParameterElement oldParameter,
@Nullable JavaMethodParameterElement newParameter) {
}
/**
* Please override the
* {@link #doVisitField(JavaFieldElement, JavaFieldElement)}
* instead.
*
* @see Check#visitField(JavaFieldElement, JavaFieldElement)
*/
@Override
public final void visitField(@Nullable JavaFieldElement oldField, @Nullable JavaFieldElement newField) {
depth++;
doVisitField(oldField, newField);
}
protected void doVisitField(@Nullable JavaFieldElement oldField, @Nullable JavaFieldElement newField) {
}
/**
* Please override the
* {@link #doVisitAnnotation(JavaAnnotationElement, JavaAnnotationElement)}
* instead.
*
* @see Check#visitAnnotation(JavaAnnotationElement, JavaAnnotationElement)
*/
@Nullable
@Override
public final List<Difference> visitAnnotation(@Nullable JavaAnnotationElement oldAnnotation,
@Nullable JavaAnnotationElement newAnnotation) {
depth++;
List<Difference> ret = doVisitAnnotation(oldAnnotation, newAnnotation);
depth--;
return ret;
}
@Nullable
protected List<Difference> doVisitAnnotation(@Nullable JavaAnnotationElement oldAnnotation,
@Nullable JavaAnnotationElement newAnnotation) {
return null;
}
/**
* If called in one of the {@code doVisit*()} methods, this method will push the elements along with some
* contextual
* data onto an internal stack.
*
* <p>You can then retrieve the contents on the top of the stack in your {@link #doEnd()} override by calling the
* {@link #popIfActive()} method.
*
* @param oldElement the old API element
* @param newElement the new API element
* @param context optional contextual data
* @param <T> the type of the elements
*/
protected final <T extends JavaElement> void pushActive(@Nullable T oldElement, @Nullable T newElement,
Object... context) {
ActiveElements<T> r = new ActiveElements<>(depth, oldElement, newElement, context);
activations.push(r);
}
/**
* Pops the top of the stack of active elements if the current position in the call stack corresponds to the one
* that pushed the active elements.
*
* <p>This method does not do any type checks, so take care to retrieve the elements with the same types used to push
* to them onto the stack.
*
* @param <T> the type of the elements
*
* @return the active elements or null if the current call stack did not push any active elements onto the stack
*/
@Nullable
@SuppressWarnings("unchecked")
protected <T extends JavaElement> ActiveElements<T> popIfActive() {
return (ActiveElements<T>) (!activations.isEmpty() && activations.peek().depth == depth ? activations.pop() :
null);
}
}