/*
* Copyright 2014 <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
*
* 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.jboss.windup.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.jboss.forge.furnace.spi.ListenerRegistration;
import org.jboss.windup.config.metadata.RuleMetadataType;
import org.jboss.windup.config.phase.RulePhase;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.model.WindupExecutionModel;
import org.jboss.windup.graph.model.performance.RulePhaseExecutionStatisticsModel;
import org.jboss.windup.graph.model.performance.RuleProviderExecutionStatisticsModel;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.graph.service.RuleProviderExecutionStatisticsService;
import org.jboss.windup.util.exception.WindupException;
import org.jboss.windup.util.exception.WindupStopException;
import org.ocpsoft.common.util.Assert;
import org.ocpsoft.rewrite.bind.Binding;
import org.ocpsoft.rewrite.bind.Evaluation;
import org.ocpsoft.rewrite.config.CompositeOperation;
import org.ocpsoft.rewrite.config.CompositeRule;
import org.ocpsoft.rewrite.config.Condition;
import org.ocpsoft.rewrite.config.ConditionVisit;
import org.ocpsoft.rewrite.config.Configuration;
import org.ocpsoft.rewrite.config.DefaultOperationBuilder;
import org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.config.OperationVisit;
import org.ocpsoft.rewrite.config.ParameterizedCallback;
import org.ocpsoft.rewrite.config.ParameterizedConditionVisitor;
import org.ocpsoft.rewrite.config.ParameterizedOperationVisitor;
import org.ocpsoft.rewrite.config.Rule;
import org.ocpsoft.rewrite.config.RuleBuilder;
import org.ocpsoft.rewrite.context.Context;
import org.ocpsoft.rewrite.context.ContextBase;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.context.RewriteState;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.param.ConfigurableParameter;
import org.ocpsoft.rewrite.param.Constraint;
import org.ocpsoft.rewrite.param.DefaultParameter;
import org.ocpsoft.rewrite.param.DefaultParameterStore;
import org.ocpsoft.rewrite.param.DefaultParameterValueStore;
import org.ocpsoft.rewrite.param.Parameter;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.ParameterValueStore;
import org.ocpsoft.rewrite.param.Parameterized;
import org.ocpsoft.rewrite.param.Transposition;
import org.ocpsoft.rewrite.util.ParameterUtils;
import org.ocpsoft.rewrite.util.Visitor;
/**
* An {@link Operation} that allows for conditional evaluation of nested {@link Rule} sets.
*
* @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
* @author <a href="http://ondra.zizka.cz/">Ondrej Zizka, I - zizka at seznam.cz</a>
*/
public class RuleSubset extends DefaultOperationBuilder implements CompositeOperation, Parameterized, CompositeRule
{
private static final Logger log = Logger.getLogger(RuleSubset.class.getName());
/**
* This accumulates all exceptions for later usage if there is not set Halt on Exceptions
* Useful for tests or choose your use case ;)
*/
private Map<String, Exception> exceptions = new LinkedHashMap<>();
/**
* @return the exceptions
*/
public Map<String, Exception> getExceptions()
{
return exceptions;
}
/**
* Used for tracking the time taken by the rules within each RuleProvider. This links from a {@link AbstractRuleProvider} to the ID of a
* {@link RuleProviderExecutionStatisticsModel}
*/
private final IdentityHashMap<AbstractRuleProvider, Object> timeTakenByProvider = new IdentityHashMap<>();
/**
* Used for tracking the time taken by each phase of execution. This links from a {@link RulePhase} to the ID of a
* {@link RulePhaseExecutionStatisticsModel}
*/
private final Map<Class<? extends RulePhase>, Object> timeTakenByPhase = new HashMap<>();
private final Configuration config;
private final List<RuleLifecycleListener> listeners = new ArrayList<>();
private boolean alwaysHaltOnFailure = false;
private RuleSubset(Configuration config)
{
Assert.notNull(config, "Configuration must not be null.");
this.config = config;
}
public static RuleSubset create(Configuration config)
{
return new RuleSubset(config);
}
public void setAlwaysHaltOnFailure(boolean alwaysHaltOnFailure)
{
this.alwaysHaltOnFailure = alwaysHaltOnFailure;
}
/**
* Logs the time taken by this rule, and attaches this to the total for the RuleProvider
*/
private void logTimeTakenByRuleProvider(GraphContext graphContext, Context context, int ruleIndex, int timeTaken)
{
AbstractRuleProvider ruleProvider = (AbstractRuleProvider) context.get(RuleMetadataType.RULE_PROVIDER);
if (ruleProvider == null)
return;
if (!timeTakenByProvider.containsKey(ruleProvider))
{
RuleProviderExecutionStatisticsModel model = new RuleProviderExecutionStatisticsService(graphContext)
.create();
model.setRuleIndex(ruleIndex);
model.setRuleProviderID(ruleProvider.getMetadata().getID());
model.setTimeTaken(timeTaken);
timeTakenByProvider.put(ruleProvider, model.asVertex().getId());
}
else
{
RuleProviderExecutionStatisticsService service = new RuleProviderExecutionStatisticsService(graphContext);
RuleProviderExecutionStatisticsModel model = service.getById(timeTakenByProvider.get(ruleProvider));
int prevTimeTaken = model.getTimeTaken();
model.setTimeTaken(prevTimeTaken + timeTaken);
}
logTimeTakenByPhase(graphContext, ruleProvider.getMetadata().getPhase(), timeTaken);
}
/**
* Logs the time taken by this rule and adds this to the total time taken for this phase
*/
private void logTimeTakenByPhase(GraphContext graphContext, Class<? extends RulePhase> phase, int timeTaken)
{
if (!timeTakenByPhase.containsKey(phase))
{
RulePhaseExecutionStatisticsModel model = new GraphService<>(graphContext,
RulePhaseExecutionStatisticsModel.class).create();
model.setRulePhase(phase.toString());
model.setTimeTaken(timeTaken);
model.setOrderExecuted(timeTakenByPhase.size());
timeTakenByPhase.put(phase, model.asVertex().getId());
}
else
{
GraphService<RulePhaseExecutionStatisticsModel> service = new GraphService<>(graphContext,
RulePhaseExecutionStatisticsModel.class);
RulePhaseExecutionStatisticsModel model = service.getById(timeTakenByPhase.get(phase));
int prevTimeTaken = model.getTimeTaken();
model.setTimeTaken(prevTimeTaken + timeTaken);
}
}
@Override
public void perform(Rewrite rewrite, EvaluationContext context)
{
if (!(rewrite instanceof GraphRewrite))
throw new IllegalArgumentException("Rewrite must be an instanceof GraphRewrite");
/*
* Highly optimized loop - for performance reasons. Think before you change this! (lincolnthree)
*/
GraphRewrite event = (GraphRewrite) rewrite;
List<Rule> rules = config.getRules();
for (RuleLifecycleListener listener : listeners)
{
listener.beforeExecution(event);
}
EvaluationContextImpl subContext = new EvaluationContextImpl();
rulesLoop:
for (int i = 0; i < rules.size(); i++)
{
Rule rule = rules.get(i);
Context ruleContext = rule instanceof Context ? (Context) rule : null;
long ruleTimeStarted = System.currentTimeMillis();
try
{
AbstractRuleProvider ruleProvider = (AbstractRuleProvider) ruleContext.get(RuleMetadataType.RULE_PROVIDER);
if (ruleProvider != null && ruleProvider.getMetadata() != null && ruleProvider.getMetadata().isDisabled())
{
log.info("RuleProvider is disabled, skipping: " + ruleProvider.getMetadata().getID());
continue;
}
subContext = new EvaluationContextImpl();
ParameterStore parameterStore = (ParameterStore) context.get(ParameterStore.class);
if (parameterStore == null)
parameterStore = new DefaultParameterStore();
subContext.put(ParameterStore.class, parameterStore);
setParameterStore(parameterStore);
ParameterValueStore values = (ParameterValueStore) context.get(ParameterValueStore.class);
if (values == null)
values = new DefaultParameterValueStore();
subContext.put(ParameterValueStore.class, values);
subContext.setState(RewriteState.EVALUATING);
subContext.put(Rule.class, rule);
Variables.instance(event).push();
try
{
for (RuleLifecycleListener listener : listeners)
{
boolean windupStopRequested = listener.beforeRuleEvaluation(event, rule, subContext);
if (windupStopRequested)
{
String msg = "Windup was requested to stop before beforeRuleEvaluation() of " + rule.getId() + ", skipping further rules.";
log.warning(msg);
event.setWindupStopException(new WindupStopException(msg));
break rulesLoop;
}
}
if (rule.evaluate(event, subContext))
{
for (RuleLifecycleListener listener : listeners)
{
listener.afterRuleConditionEvaluation(event, subContext, rule, true);
}
if (!handleBindings(event, subContext, values))
continue;
subContext.setState(RewriteState.PERFORMING);
final Object ruleProviderDesc = ((RuleBuilder) rule).get(RuleMetadataType.RULE_PROVIDER);
log.info("Rule [" + ruleProviderDesc + "] matched and will be performed.");
for (RuleLifecycleListener listener : listeners)
{
boolean windupStopRequested = listener.beforeRuleOperationsPerformed(event, subContext, rule);
if (windupStopRequested)
{
String msg = "Windup was requested to stop before beforeRuleOperationsPerformed() of " + rule.getId() + ", skipping further rules.";
log.warning(msg);
event.setWindupStopException(new WindupStopException(msg));
break rulesLoop;
}
}
List<Operation> preOperations = subContext.getPreOperations();
for (Operation preOperation : preOperations)
{
preOperation.perform(event, subContext);
}
if (event.getFlow().isHandled())
break;
rule.perform(event, subContext);
for (RuleLifecycleListener listener : listeners)
{
listener.afterRuleOperationsPerformed(event, subContext, rule);
}
if (event.getFlow().isHandled())
break;
List<Operation> postOperations = subContext.getPostOperations();
for (Operation postOperation : postOperations)
{
postOperation.perform(event, subContext);
}
if (event.getFlow().isHandled())
break;
}
else
{
for (RuleLifecycleListener listener : listeners)
{
listener.afterRuleConditionEvaluation(event, subContext, rule, false);
}
}
}
catch (WindupStopException ex)
{
final String msg = "Windup was requested to stop during execution of " + rule.getId() + ", skipping further rules.";
log.warning(msg);
event.setWindupStopException(new WindupStopException(msg, ex));
event.getGraphContext().service(WindupExecutionModel.class).create().setStopMessage(msg);
break rulesLoop;
}
finally
{
boolean autocommit = true;
if (ruleContext != null && ruleContext.containsKey(RuleMetadataType.AUTO_COMMIT))
autocommit = (Boolean) ruleContext.get(RuleMetadataType.AUTO_COMMIT);
if (autocommit)
event.getGraphContext().getGraph().getBaseGraph().commit();
Variables.instance(event).pop();
long ruleTimeCompleted = System.currentTimeMillis();
if (ruleContext != null)
{
int timeTaken = (int) (ruleTimeCompleted - ruleTimeStarted);
logTimeTakenByRuleProvider(event.getGraphContext(), ruleContext, i, timeTaken);
}
}
}
catch (RuntimeException ex)
{
for (RuleLifecycleListener listener : listeners)
{
listener.afterRuleExecutionFailed(event, subContext, rule, ex);
}
String exMsg = "Error encountered while evaluating rule: " + rule;
String logMsg = exMsg + "\n" + StringUtils.defaultString(ex.getMessage(), "(Exception message is not set)");
log.log(Level.SEVERE, logMsg, ex);
if (ruleContext != null)
{
Object origin = ruleContext.get(RuleMetadataType.ORIGIN);
if (origin != null)
exMsg += "\n From: " + origin;
Object location = ruleContext.get(org.ocpsoft.rewrite.config.RuleMetadata.PROVIDER_LOCATION);
if (location != null)
exMsg += "\n Defined in: " + location;
}
// Depending on RuleProvider's haltOnException, halt Windup on exception.
AbstractRuleProvider ruleProvider = (AbstractRuleProvider) ruleContext.get(RuleMetadataType.RULE_PROVIDER);
boolean halt = alwaysHaltOnFailure || ruleProvider.getMetadata().isHaltOnException();
Object halt_ = ruleContext.get(RuleMetadataType.HALT_ON_EXCEPTION);
halt |= (halt_ instanceof Boolean && ((Boolean) halt_).booleanValue());
if (halt)
throw new WindupException(exMsg, ex);
else
exceptions.put(rule.getId(), ex);
}
}
if (event.getWindupStopException() == null)
for (RuleLifecycleListener listener : listeners)
listener.afterExecution(event);
}
private boolean handleBindings(final Rewrite event, final EvaluationContextImpl context,
ParameterValueStore valueStore)
{
boolean result = true;
ParameterStore store = (ParameterStore) context.get(ParameterStore.class);
for (Entry<String, Parameter<?>> entry : store)
{
Parameter<?> parameter = entry.getValue();
String values = valueStore.retrieve(parameter);
if (!ParameterUtils.enqueueSubmission(event, context, parameter, values))
{
result = false;
break;
}
}
return result;
}
/*
* Getters
*/
@Override
public List<Operation> getOperations()
{
return Collections.emptyList();
}
private static class EvaluationContextImpl extends ContextBase implements EvaluationContext
{
private final List<Operation> preOperations = new ArrayList<>();
private final List<Operation> postOperations = new ArrayList<>();
private RewriteState state;
public EvaluationContextImpl()
{
put(ParameterStore.class, new DefaultParameterStore());
}
@Override
public void addPreOperation(final Operation operation)
{
this.preOperations.add(operation);
}
@Override
public void addPostOperation(final Operation operation)
{
this.preOperations.add(operation);
}
/**
* Get an immutable view of the added pre-{@link Operation} instances.
*/
public List<Operation> getPreOperations()
{
return Collections.unmodifiableList(preOperations);
}
/**
* Get an immutable view of the added post-{@link Operation} instances.
*/
public List<Operation> getPostOperations()
{
return Collections.unmodifiableList(postOperations);
}
@Override
public String toString()
{
return "EvaluationContextImpl [preOperations=" + preOperations + ", postOperations=" + postOperations + "]";
}
@Override
public RewriteState getState()
{
return state;
}
public void setState(RewriteState state)
{
this.state = state;
}
}
@Override
public Set<String> getRequiredParameterNames()
{
return Collections.emptySet();
}
@Override
public void setParameterStore(final ParameterStore parent)
{
for (int i = 0; i < config.getRules().size(); i++)
{
Rule rule = config.getRules().get(i);
if (!(rule instanceof RuleBuilder))
continue;
ParameterizedCallback callback = new ParameterizedCallbackImpl(rule, parent);
Visitor<Condition> conditionVisitor = new ParameterizedConditionVisitor(callback);
new ConditionVisit(rule).accept(conditionVisitor);
Visitor<Operation> operationVisitor = new ParameterizedOperationVisitor(callback);
new OperationVisit(rule).accept(operationVisitor);
}
}
/**
*
*/
private static class ParameterizedCallbackImpl implements ParameterizedCallback
{
private final Rule rule;
private final ParameterStore parent;
public ParameterizedCallbackImpl(Rule rule, ParameterStore parent)
{
this.rule = rule;
this.parent = parent;
}
@Override
public void call(Parameterized parameterized)
{
Set<String> names = parameterized.getRequiredParameterNames();
if (!(rule instanceof RuleBuilder))
return;
ParameterStore store = ((RuleBuilder) rule).getParameterStore();
for (Entry<String, Parameter<?>> entry : parent)
{
String name = entry.getKey();
Parameter<?> parentParam = entry.getValue();
if (!store.contains(name))
{
store.get(name, parentParam);
continue;
}
Parameter<?> parameter = store.get(name);
for (Binding binding : parameter.getBindings())
{
if (!parentParam.getBindings().contains(binding))
throwRedefinitionError(rule, name);
}
for (Constraint<String> constraint : parameter.getConstraints())
{
if (!parentParam.getConstraints().contains(constraint))
throwRedefinitionError(rule, name);
}
for (Transposition<String> transposition : parameter.getTranspositions())
{
if (!parentParam.getTranspositions().contains(transposition))
throwRedefinitionError(rule, name);
}
if (parentParam.getConverter() != null
&& !parentParam.getConverter().equals(parameter.getConverter()))
throwRedefinitionError(rule, name);
if (parentParam.getValidator() != null
&& !parentParam.getValidator().equals(parameter.getValidator()))
throwRedefinitionError(rule, name);
}
for (String name : names)
{
Parameter<?> parameter = store.get(name, new DefaultParameter(name));
if (parameter instanceof ConfigurableParameter<?>)
((ConfigurableParameter<?>) parameter).bindsTo(Evaluation.property(name));
}
parameterized.setParameterStore(store);
}
private void throwRedefinitionError(Rule rule, String name)
{
throw new IllegalStateException("Subset cannot re-configure parameter [" + name
+ "] that was configured in parent Configuration. Re-definition was attempted at ["
+ rule + "] ");
}
}
/**
* Add a {@link RuleLifecycleListener} to receive events when {@link Rule} instances are evaluated, executed, and their results.
*/
public ListenerRegistration<RuleLifecycleListener> addLifecycleListener(final RuleLifecycleListener listener)
{
this.listeners.add(listener);
return new ListenerRegistration<RuleLifecycleListener>()
{
@Override
public RuleLifecycleListener removeListener()
{
listeners.remove(listener);
return listener;
}
};
}
@Override
public String getId()
{
return "RuleSubset_" + config.hashCode();
}
@Override
public boolean evaluate(Rewrite event, EvaluationContext context)
{
return config != null && config.getRules() != null && !config.getRules().isEmpty();
}
@Override
public List<Rule> getRules()
{
return config == null ? null : config.getRules();
}
@Override
public String toString()
{
return "RuleSubset.create(" + this.config + ")";
}
}