/**
* Copyright (C) 2006-2017 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.chain;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import spoon.Launcher;
import spoon.SpoonException;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.visitor.Filter;
import spoon.reflect.visitor.filter.CtScannerFunction;
/**
* The facade of {@link CtQuery} which represents a query bound to the {@link CtElement},
* which is the constant input of this query.
* It is used by {@link CtElement} implementations of {@link CtQueryable}.
*/
public class CtQueryImpl implements CtQuery {
/**
* All the constant inputs of this query.
*/
private List<Object> inputs;
private OutputFunctionWrapper outputStep = new OutputFunctionWrapper();
private AbstractStep lastStep = outputStep;
private AbstractStep firstStep = lastStep;
private boolean terminated = false;
public CtQueryImpl(Object... input) {
setInput(input);
}
/**
* @return list of elements which will be used as input of the query
*/
public List<Object> getInputs() {
return inputs == null ? Collections.emptyList() : inputs;
}
@SuppressWarnings("unchecked")
@Override
public CtQueryImpl setInput(Object... input) {
if (inputs != null) {
inputs.clear();
}
return addInput(input);
}
/**
* adds list of elements which will be used as input of the query too
* @param input
* @return this to support fluent API
*/
public CtQueryImpl addInput(Object... input) {
if (this.inputs == null) {
this.inputs = new ArrayList<>();
}
if (input != null) {
for (Object in : input) {
this.inputs.add(in);
}
}
return this;
}
@Override
public <R> void forEach(CtConsumer<R> consumer) {
outputStep.setNext(consumer);
for (Object input : inputs) {
firstStep.accept(input);
}
}
@SuppressWarnings("unchecked")
@Override
public <R extends Object> List<R> list() {
return (List<R>) list(Object.class);
}
@Override
public <R> List<R> list(final Class<R> itemClass) {
final List<R> list = new ArrayList<>();
forEach(new CtConsumer<R>() {
@Override
public void accept(R out) {
if (out != null && itemClass.isAssignableFrom(out.getClass())) {
list.add(out);
}
}
});
return list;
}
@SuppressWarnings("unchecked")
@Override
public <R> R first() {
return (R) first(Object.class);
}
@SuppressWarnings("unchecked")
@Override
public <R> R first(final Class<R> itemClass) {
final Object[] result = new Object[1];
outputStep.setNext(new CtConsumer<R>() {
@Override
public void accept(R out) {
if (out != null && itemClass.isAssignableFrom(out.getClass())) {
result[0] = out;
terminate();
}
}
});
for (Object input : inputs) {
firstStep.accept(input);
if (isTerminated()) {
break;
}
}
return (R) result[0];
}
private boolean logging = false;
private QueryFailurePolicy failurePolicy = QueryFailurePolicy.FAIL;
@Override
public <I> CtQueryImpl map(CtConsumableFunction<I> code) {
addStep(new LazyFunctionWrapper(code));
return this;
}
@Override
public <I, R> CtQueryImpl map(CtFunction<I, R> function) {
addStep(new FunctionWrapper(function));
return this;
}
@Override
public <R extends CtElement> CtQueryImpl filterChildren(Filter<R> filter) {
map(new CtScannerFunction());
if (filter != null) {
select(filter);
}
return this;
}
@Override
public <R extends CtElement> CtQueryImpl select(final Filter<R> filter) {
map(new CtFunction<R, Boolean>() {
@Override
public Boolean apply(R input) {
return filter.matches(input);
}
});
stepFailurePolicy(QueryFailurePolicy.IGNORE);
return this;
}
@Override
public boolean isTerminated() {
return terminated;
}
@Override
public void terminate() {
terminated = true;
}
/**
* Evaluates this query, ignoring bound input - if any
*
* @param input represents the input element of the first mapping function of this query
* @param outputConsumer method accept of the outputConsumer is called for each element produced by last mapping function of this query
*/
public <I, R> void evaluate(I input, CtConsumer<R> outputConsumer) {
outputStep.setNext(outputConsumer);
firstStep.accept(input);
}
@Override
public CtQueryImpl name(String name) {
lastStep.setName(name);
return this;
}
@Override
public CtQueryImpl failurePolicy(QueryFailurePolicy policy) {
failurePolicy = policy;
return this;
}
public CtQueryImpl stepFailurePolicy(QueryFailurePolicy policy) {
lastStep.setLocalFailurePolicy(policy);
return this;
}
/**
* Enable/disable logging for this query
*
* Note: it is not possible to enable logging of all queries globally by Launcher.LOGGER.isDebugEnabled()
* because it causes StackOverflow.
* Reason: Query chains are used internally during writing of log messages too. So it would write logs for ever...
*/
public CtQueryImpl logging(boolean logging) {
this.logging = logging;
return this;
}
protected void handleListenerSetQuery(Object target) {
if (target instanceof CtQueryAware) {
((CtQueryAware) target).setQuery(this);
}
}
private void addStep(AbstractStep step) {
step.nextStep = outputStep;
lastStep.nextStep = step;
lastStep = step;
if (firstStep == outputStep) {
firstStep = step;
}
step.setName(String.valueOf(getStepIndex(step) + 1));
}
private int getStepIndex(AbstractStep step) {
int idx = 0;
AbstractStep s = firstStep;
while (s != outputStep) {
if (s == step) {
return idx;
}
s = (AbstractStep) s.nextStep;
idx++;
}
return -1;
}
private boolean isLogging() {
return logging;
}
/**
* Is used to log that invocation was not processed
* @param step the step which thrown CCE
* @param e
* @param parameters
*/
private void onClassCastException(AbstractStep step, ClassCastException e, Object... parameters) {
if (step.isFailOnCCE()) {
throw new SpoonException(getStepDescription(step, e.getMessage(), parameters), e);
} else if (Launcher.LOGGER.isTraceEnabled()) {
//log expected CCE ... there might be some unexpected too!
Launcher.LOGGER.trace(e);
}
log(step, e.getMessage(), parameters);
}
private void log(AbstractStep step, String message, Object... parameters) {
if (isLogging() && Launcher.LOGGER.isInfoEnabled()) {
Launcher.LOGGER.info(getStepDescription(step, message, parameters));
}
}
private String getStepDescription(AbstractStep step, String message, Object... parameters) {
StringBuilder sb = new StringBuilder("Step ");
sb.append(step.getName()).append(") ");
sb.append(message);
for (int i = 0; i < parameters.length; i++) {
sb.append("\nParameter ").append(i + 1).append(") ");
if (parameters[i] != null) {
sb.append(parameters[i].getClass().getSimpleName());
sb.append(": ");
}
sb.append(parameters[i]);
}
return sb.toString();
}
/**
* Holds optional name and local QueryFailurePolicy of each step
*/
private abstract class AbstractStep implements CtConsumer<Object> {
String name;
QueryFailurePolicy localFailurePolicy = null;
CtConsumer<Object> nextStep;
/**
* @return name of this Step - for debugging purposes
*/
private String getName() {
return name;
}
/**
* @param name of the step - for debugging purposes
*/
private void setName(String name) {
this.name = name;
}
/**
* @return true if this step should throw {@link ClassCastException} in case of
* step input type incompatibility
*/
private boolean isFailOnCCE() {
if (localFailurePolicy != null) {
return localFailurePolicy == QueryFailurePolicy.FAIL;
} else {
return failurePolicy == QueryFailurePolicy.FAIL;
}
}
private void setLocalFailurePolicy(QueryFailurePolicy localFailurePolicy) {
this.localFailurePolicy = localFailurePolicy;
}
}
/**
* Wrapper around terminal {@link CtConsumer}, which accepts output of this query
*/
private class OutputFunctionWrapper extends AbstractStep {
@Override
public void accept(Object element) {
if (element == null || isTerminated()) {
return;
}
try {
nextStep.accept(element);
} catch (ClassCastException e) {
if (Launcher.LOGGER.isTraceEnabled()) {
//log expected CCE ... there might be some unexpected too!
Launcher.LOGGER.trace(e);
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
<R> void setNext(CtConsumer<R> out) {
//we are preparing new query execution.
reset();
nextStep = (CtConsumer) out;
handleListenerSetQuery(nextStep);
}
}
/**
* Called before query is evaluated again
*/
protected void reset() {
terminated = false;
}
private class LazyFunctionWrapper extends AbstractStep {
private final CtConsumableFunction<Object> fnc;
@SuppressWarnings("unchecked")
LazyFunctionWrapper(CtConsumableFunction<?> fnc) {
super();
this.fnc = (CtConsumableFunction<Object>) fnc;
handleListenerSetQuery(this.fnc);
}
@Override
public void accept(Object input) {
if (input == null || isTerminated()) {
return;
}
try {
fnc.apply(input, nextStep);
} catch (ClassCastException e) {
onClassCastException(this, e, input);
return;
}
}
}
/**
* a step which calls Function. Implements contract of {@link CtQuery#map(CtFunction)}
*/
private class FunctionWrapper extends AbstractStep {
private CtFunction<Object, Object> fnc;
@SuppressWarnings("unchecked")
FunctionWrapper(CtFunction<?, ?> code) {
super();
fnc = (CtFunction<Object, Object>) code;
handleListenerSetQuery(fnc);
}
@SuppressWarnings("unchecked")
@Override
public void accept(Object input) {
if (input == null || isTerminated()) {
return;
}
Object result;
try {
result = fnc.apply(input);
} catch (ClassCastException e) {
onClassCastException(this, e, input);
return;
}
if (result == null || isTerminated()) {
return;
}
if (result instanceof Boolean) {
//the code is a predicate. send the input to output if result is true
if ((Boolean) result) {
nextStep.accept(input);
} else {
log(this, "Skipped element, because CtFunction#accept(input) returned false", input);
}
return;
}
if (result instanceof Iterable) {
//send each item of Iterable to the next step
for (Object out : (Iterable<Object>) result) {
nextStep.accept(out);
if (isTerminated()) {
return;
}
}
return;
}
if (result.getClass().isArray()) {
//send each item of Array to the next step
for (int i = 0; i < Array.getLength(result); i++) {
nextStep.accept(Array.get(result, i));
if (isTerminated()) {
return;
}
}
return;
}
nextStep.accept(result);
}
}
}