diff --git a/src/main/java/spoon/reflect/declaration/CtElement.java b/src/main/java/spoon/reflect/declaration/CtElement.java index c291b3175da..1a7b2ce3474 100644 --- a/src/main/java/spoon/reflect/declaration/CtElement.java +++ b/src/main/java/spoon/reflect/declaration/CtElement.java @@ -23,6 +23,7 @@ import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.CtVisitable; import spoon.reflect.visitor.Filter; +import spoon.reflect.visitor.QueryBuilder; import spoon.reflect.visitor.Root; import spoon.support.DerivedProperty; @@ -169,6 +170,16 @@ List getAnnotatedChildren( */ List getElements(Filter filter); + /** + * Returns QueryBuilder which will process query starting on this Element using Filter filter + * Use the {@QueryBuilder} fluent API to build more complex filter or use + *
    + *
  • {@QueryBuilder#list()} to return List of all matches + *
  • {@QueryBuilder#forEach(consumer)} to call consumer for each match + *
+ */ + QueryBuilder query(Filter filter); + /** * @param filter * @return diff --git a/src/main/java/spoon/reflect/visitor/ElementConsumer.java b/src/main/java/spoon/reflect/visitor/ElementConsumer.java new file mode 100644 index 00000000000..403713b3661 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/ElementConsumer.java @@ -0,0 +1,29 @@ +/** + * Copyright (C) 2006-2016 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program 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 CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.reflect.visitor; + +import spoon.reflect.declaration.CtElement; + +/** + * Functional interface used to accept elements. + * It is used for example by {@link QueryBuilder} and {@link CtElement#query(Filter)} + * + * @param - the type of accepted element + */ +public interface ElementConsumer { + void accept(T t); +} diff --git a/src/main/java/spoon/reflect/visitor/Query.java b/src/main/java/spoon/reflect/visitor/Query.java index 87d0980f3c2..f8d0e4bd371 100644 --- a/src/main/java/spoon/reflect/visitor/Query.java +++ b/src/main/java/spoon/reflect/visitor/Query.java @@ -43,7 +43,7 @@ private Query() { * the filter which defines search scope and the matching criteria */ public static List getElements(ScopeAwareFilter filter) { - return getElements(filter.getSearchScope(), filter); + return new QueryBuilder<>().then(filter).list(); } @@ -76,9 +76,7 @@ public static List getElements(Factory factory, */ public static List getElements( CtElement rootElement, Filter filter) { - QueryVisitor visitor = new QueryVisitor<>(filter); - visitor.scan(rootElement); - return visitor.getResult(); + return new QueryBuilder<>().onInput(rootElement).then(filter).list(); } /** @@ -118,4 +116,5 @@ public static List getReferences( return getElements(factory, filter); } + } diff --git a/src/main/java/spoon/reflect/visitor/QueryBuilder.java b/src/main/java/spoon/reflect/visitor/QueryBuilder.java new file mode 100644 index 00000000000..4ae28df7fde --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/QueryBuilder.java @@ -0,0 +1,161 @@ +/** + * Copyright (C) 2006-2016 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program 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 CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.reflect.visitor; + +import java.util.ArrayList; +import java.util.List; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtElement; + +/** + * Builder which can be used to build chain of Filters. The query built using this QueryBuilder uses this algorithm: + *
    + *
  1. all the input elements registered by {@link #onInput(CtElement...)} are used to start query on first filter of the chain. See details below. + *
  2. if there is next filter, then all matching elements, which pass filter X will be collected as input for the next filter + *
  3. if there is no next filter, then all matching elements are sent to outputConsumer + *
+ * + * The input elements are used in query of filter like this: + *
    + *
  • In case of Filter which implements {@link ChainableFilter}, the input will be set as input parameter of the filter and then the Filter computes starting element of AST traversal + *
  • In case of Filter which implements {@link ScopeAwareFilter}, the input will be ignored, but this Filter computes starting element of AST traversal + *
  • In other cases, the input will be used as starting element of AST traversal + *
+ * + * Example of usage: + *
+ * List<Y> list = new QueryBuilder<>().onInput(anElement).then(filter1<X>).then(filterN<Y>).list(); + *
+ * new QueryBuilder<>().onInput(anElement).then(filter1<X>).then(filterN<Y>).forEach(Y element -> ..process elements...); + * + * Note: If the first filter implements {@link ScopeAwareFilter}, then you can run query without previous call of {@link #onInput(CtElement...)}. + * The search scope will be computed by first filter in such case. + * + * @param defines type of returned elements. It is equal to type of last filter in the chain + */ +public class QueryBuilder { + + private ArrayList inputs = new ArrayList<>(); + private List> chainItems = new ArrayList<>(); + + public QueryBuilder() { + } + + /** + * @param input - one or more input elements + * @return this instance, so it can be used for next query building. + */ + public QueryBuilder onInput(CtElement... input) { + for (CtElement element : input) { + inputs.add(element); + } + return this; + } + + /** + * Adds next filter into chain of filters. + * + * @param filter + * @return this instance, so it can be used for next query building. + */ + @SuppressWarnings("unchecked") + public QueryBuilder then(Filter filter) { + chainItems.add(filter); + return (QueryBuilder) this; + } + + /** + * @return ElementConsumer which contains whole chain of built filters and which finally will call outputConsumer + */ + public ElementConsumer build(ElementConsumer outputConsumer) { + return build(outputConsumer, 0); + } + + /** + * @param outputConsumer - the consumer which should be used as last one in the chain + * @param level + * @return the ElementConsumer of the level level. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected ElementConsumer build(ElementConsumer outputConsumer, int level) { + if (chainItems.size() == level) { + //there is no filter for this level. return outputConsumer + return (ElementConsumer) outputConsumer; + } + final Filter filter = (Filter) chainItems.get(level); + final QueryVisitor queryVisitor = new QueryVisitor(filter, build(outputConsumer, level + 1)); + return new ElementConsumer() { + @Override + public void accept(CtElement inputElement) { + if (filter instanceof ChainableFilter && inputElement != null) { + try { + ((ChainableFilter) filter).setFilterInput(inputElement); + } catch (ClassCastException e) { + //the filter does not accepts this as input. Ignore this not matching element + return; + } + } + if (filter instanceof ScopeAwareFilter) { + queryVisitor.scan(((ScopeAwareFilter) filter).getSearchScope()); + } else { + queryVisitor.scan(inputElement); + } + } + }; + } + + /** + * Process all registered filters on all input elements and return list of matching elements + * @return the result of the query as a List of elements + */ + public List list() { + final List list = new ArrayList<>(); + forEach(new ElementConsumer() { + @Override + public void accept(T element) { + list.add(element); + } + }); + return list; + } + + /** + * Process all registered filters on all input elements and send matching elements to consumer + * @param consumer + */ + public void forEach(ElementConsumer consumer) { + ElementConsumer myConsumer = build(consumer); + if (inputs.isEmpty()) { + //there is no input element defined + if (chainItems.isEmpty()) { + //there is no filter defined. Do not call consumer + return; + } + if (chainItems.get(0) instanceof ScopeAwareFilter) { + //first filter is ScopeAwareFilter, so it computes start element of AST traversal. Use null as start element + myConsumer.accept(null); + } else { + throw new SpoonException("The QueryBuilder is missing the input element. Call QueryBuilder.onInput(element)"); + } + } else { + for (CtElement element : inputs) { + myConsumer.accept(element); + } + } + } +} diff --git a/src/main/java/spoon/reflect/visitor/QueryVisitor.java b/src/main/java/spoon/reflect/visitor/QueryVisitor.java index 83487e415ea..c710464158e 100644 --- a/src/main/java/spoon/reflect/visitor/QueryVisitor.java +++ b/src/main/java/spoon/reflect/visitor/QueryVisitor.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import spoon.SpoonException; import spoon.reflect.declaration.CtElement; import spoon.reflect.visitor.filter.AbstractFilter; import spoon.support.util.RtHelper; @@ -30,13 +31,21 @@ public class QueryVisitor extends CtScanner { private final Filter filter; private final Class filteredType; - private final List result = new ArrayList<>(); + private final ElementConsumer consumer; /** * Constructs a query visitor with a given filter. + * @deprecated use {@link QueryBuilder} instead. */ - @SuppressWarnings("unchecked") + @Deprecated public QueryVisitor(Filter filter) { + this(filter, new ArrayConsumer()); + } + + /** + * Constructs a query visitor with a given filter and Consumer. + */ + public QueryVisitor(Filter filter, ElementConsumer consumer) { super(); this.filter = filter; if (filter instanceof AbstractFilter) { @@ -45,13 +54,28 @@ public QueryVisitor(Filter filter) { Class[] params = RtHelper.getMethodParameterTypes(filter.getClass(), "matches", 1); filteredType = (Class) params[0]; } + this.consumer = consumer; + } + + //TODO remove with deprecated methods + private static class ArrayConsumer implements ElementConsumer { + final List result = new ArrayList<>(); + @Override + public void accept(T e) { + result.add(e); + } } /** * Gets the result (elements matching the filter). + * @deprecated use {@link QueryBuilder} instead. */ + @Deprecated public List getResult() { - return result; + if (consumer instanceof ArrayConsumer) { + return ((ArrayConsumer) consumer).result; + } + throw new SpoonException("getResult() cannot be used on QueryVistor with consumer"); } @SuppressWarnings("unchecked") @@ -63,7 +87,7 @@ public void scan(CtElement element) { try { if ((filteredType == null || filteredType.isAssignableFrom(element.getClass()))) { if (filter.matches((T) element)) { - result.add((T) element); + consumer.accept((T) element); } } } catch (ClassCastException e) { diff --git a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java index bf51dfa290a..a1094ebe4dc 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java @@ -31,6 +31,7 @@ import spoon.reflect.visitor.Filter; import spoon.reflect.visitor.ModelConsistencyChecker; import spoon.reflect.visitor.Query; +import spoon.reflect.visitor.QueryBuilder; import spoon.reflect.visitor.filter.AnnotationFilter; import spoon.support.util.EmptyClearableList; import spoon.support.util.EmptyClearableSet; @@ -254,6 +255,11 @@ public List getElements(Filter filter) { return Query.getElements(this, filter); } + @Override + public QueryBuilder query(Filter filter) { + return new QueryBuilder<>().onInput(this).then(filter); + } + public List getReferences(Filter filter) { return getElements(filter); }