/* * Copyright 2016 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.filters; import static java.util.stream.Collectors.toList; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.jboss.dmr.ModelNode; import org.revapi.AnalysisContext; import org.revapi.Element; import org.revapi.ElementFilter; import org.revapi.java.spi.JavaAnnotationElement; import org.revapi.java.spi.JavaElement; import org.revapi.java.spi.JavaModelElement; /** * @author Lukas Krejci * @since 0.7.0 */ abstract class AbstractIncludeExcludeFilter implements ElementFilter { private final String configurationRootPath; private final String[] configurationRoot; private final String schemaPath; private final IdentityHashMap<Object, InclusionState> elementResults = new IdentityHashMap<>(); protected Predicate<String> includeTest; protected Predicate<String> excludeTest; protected boolean doNothing; protected AbstractIncludeExcludeFilter(String configurationRootPath, String schemaPath) { this.configurationRootPath = configurationRootPath; this.configurationRoot = configurationRootPath.split("\\."); this.schemaPath = schemaPath; } protected Predicate<String> composeTest(List<String> fullMatches, List<Pattern> patterns) { if (fullMatches != null && fullMatches.size() > 0) { return s -> Collections.binarySearch(fullMatches, s) >= 0; } else if (patterns != null && patterns.size() > 0) { return s -> patterns.stream().anyMatch(p -> p.matcher(s).matches()); } else { return null; } } @Override public void close() throws Exception { elementResults.clear(); } @Override public @Nullable String[] getConfigurationRootPaths() { return new String[]{configurationRootPath}; } @Override public Reader getJSONSchema(@Nonnull String configurationRootPath) { if (this.configurationRootPath.equals(configurationRootPath)) { return new InputStreamReader(getClass().getResourceAsStream(schemaPath), Charset.forName("UTF-8")); } else { return null; } } @Override public void initialize(@Nonnull AnalysisContext analysisContext) { ModelNode root = analysisContext.getConfiguration().get(configurationRoot); if (!root.isDefined()) { doNothing = true; return; } ModelNode regex = root.get("regex"); boolean regexes = regex.isDefined() && regex.asBoolean(); List<String> fullMatches = new ArrayList<>(); List<Pattern> patterns = new ArrayList<>(); readMatches(root.get("exclude"), regexes, fullMatches, patterns); validateConfiguration(true, fullMatches, patterns, regexes); this.excludeTest = composeTest(fullMatches, patterns); fullMatches = new ArrayList<>(); patterns = new ArrayList<>(); readMatches(root.get("include"), regexes, fullMatches, patterns); validateConfiguration(false, fullMatches, patterns, regexes); this.includeTest = composeTest(fullMatches, patterns); doNothing = includeTest == null && excludeTest == null; } protected abstract void validateConfiguration(boolean excludes, List<String> fullMatches, List<Pattern> patterns, boolean regexes); private void readMatches(ModelNode array, boolean regexes, List<String> fullMatches, List<Pattern> patterns) { if (!array.isDefined()) { return; } for (ModelNode ann : array.asList()) { String name = ann.asString(); if (regexes) { patterns.add(Pattern.compile(name)); } else { fullMatches.add(name); } } if (!regexes) { Collections.sort(fullMatches); } } @Override public boolean applies(@Nullable Element element) { return decide(element); } @Override public boolean shouldDescendInto(@Nullable Object element) { return true; } @SuppressWarnings("ConstantConditions") private boolean decide(@Nullable Object element) { //we don't exclude anything that we don't handle... if (doNothing || !(element instanceof JavaElement)) { return true; } InclusionState ret = elementResults.get(element); if (ret != null) { return ret.toBoolean(); } JavaElement el = (JavaElement) element; //exploit the fact that parent elements are always filtered before the children Element parent = el.getParent(); InclusionState parentInclusionState = parent == null ? InclusionState.UNDECIDED : elementResults.get(parent); //if we have no record of the parent inclusion, then this is a top-level class. Assume it wants to be included. if (parentInclusionState == null) { parentInclusionState = InclusionState.UNDECIDED; } //this is a java element, but not a model-based element - i.e. this is an annotation. if (!(element instanceof JavaModelElement)) { return decideAnnotation((JavaAnnotationElement) element, parentInclusionState); } JavaModelElement javaElement = (JavaModelElement) element; Stream<String> tested = getTestedElementRepresentations(javaElement); //let's first assume we're going to inherit the parent's inclusion state ret = parentInclusionState; //now see if we need to change that assumption switch (parentInclusionState) { case INCLUDED: //the parent was explicitly included in the results. We therefore only need to check if the annotations //on this element should be excluded if (excludeTest != null) { if (tested.anyMatch(s -> excludeTest.test(s))) { ret = InclusionState.EXCLUDED; } } break; case EXCLUDED: if (!canBeReIncluded(javaElement)) { break; } //the child element can be re-included, so the full suite of tests need to be run. //i.e. this fall-through is intentional. case UNDECIDED: //ok, the parent is undecided. This means we have to do the full checks on this element. List<String> testedList = null; if (includeTest != null && excludeTest != null) { testedList = tested.collect(toList()); tested = testedList.stream(); } if (includeTest != null) { //ok, there is an include test but the parent is undecided. This means that the parent actually //didn't match the include test. Let's check with this element. ret = tested.anyMatch(s -> includeTest.test(s)) ? InclusionState.INCLUDED : InclusionState.EXCLUDED; } if (excludeTest != null) { if (testedList != null) { tested = testedList.stream(); } //there is an exclude test but the parent is undecided. This means that the exclude check didn't //match the parent. Let's check again with this element. if (tested.anyMatch(s -> excludeTest.test(s))) { ret = InclusionState.EXCLUDED; } } break; } elementResults.put(element, ret); return ret.toBoolean(); } boolean decideAnnotation(JavaAnnotationElement annotation, InclusionState parentInclusionState) { //annotations cannot be annotated but it would also be awkward to check a method and NOT its annotations... //therefore we just include the annotations based on the inclusion state of the annotated element. return parentInclusionState.toBoolean(); } protected abstract boolean canBeReIncluded(JavaModelElement element); protected abstract Stream<String> getTestedElementRepresentations(JavaModelElement element); protected enum InclusionState { /** * The element was explicitly determined to be included */ INCLUDED, /** * The element was explicitly determined to be excluded */ EXCLUDED, /** * There was no precise decision possible on the element */ UNDECIDED; boolean toBoolean() { switch (this) { case INCLUDED: case UNDECIDED: return true; default: return false; } } } }