/*
* -----------------------------------------------------------------------\
* PerfCake
*
* Copyright (C) 2010 - 2016 the original author or authors.
*
* 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.perfcake.validation;
import org.perfcake.PerfCakeException;
import org.perfcake.message.Message;
import org.perfcake.message.ReceivedMessage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.TreeMap;
/**
* Validates message responses returned by {@link org.perfcake.message.sender.MessageSender}
* using a set of {@link org.perfcake.validation.MessageValidator} instances.
*
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
* @author <a href="mailto:lucie.fabrikova@gmail.com">Lucie Fabriková</a>
*/
public class ValidationManager {
private static final String OVERALL_STAT_KEY = "overall";
/**
* A map of validators: validator id => validator instance.
*/
private final Map<String, MessageValidator> validators = new TreeMap<>();
/**
* A logger.
*/
private final Logger log = LogManager.getLogger(ValidationManager.class);
/**
* An internal thread that takes one response after another and validates them.
*/
private Thread validationThread = null;
/**
* Indicates whether the validation is finished. Starts with true as there is no validation running at the beginning.
*/
private boolean finished = true;
/**
* True when all the messages were validated properly so far.
*/
private boolean allMessagesValid = true;
/**
* True when the validation is enabled.
*/
private boolean enabled = false;
/**
* Unless specified in the scenario, the validation thread has some sleep for it not to influence measurement.
* At the end, when there is nothing else to do, we can go through the remaining responses faster.
*/
private volatile boolean fastForward = false;
/**
* When true, the validation thread just waits for the input queue to become empty and ends.
*/
private boolean expectLastMessage = false;
/**
* A queue with the validation tasks.
*/
private Queue<ValidationTask> validationTasks;
/**
* Stores validation results statistics.
*/
private final Map<String, Score> statistics = new HashMap<>();
/**
* Creates a new validator manager. The message responses are store in a file queue in a temporary file.
*
* @throws PerfCakeException
* When it was not possible to initialize the message store.
*/
public ValidationManager() throws PerfCakeException {
statistics.put(OVERALL_STAT_KEY, new Score());
try {
final File tmpFile = File.createTempFile("perfcake", "queue");
tmpFile.deleteOnExit();
setQueueFile(tmpFile);
} catch (final IOException e) {
throw new PerfCakeException("Cannot create a file queue for messages to be validated: ", e);
}
}
/**
* Sets a different location of the file queue for storing message responses.
*
* @param queueFile
* The new location of the file queue.
* @throws PerfCakeException
* When it was not possible to initialize the file queue or there is a running validation.
*/
public void setQueueFile(final File queueFile) throws PerfCakeException {
if (isFinished()) {
validationTasks = new FileQueue<>(queueFile);
} else {
throw new PerfCakeException("It is not possible to change the file queue while there is a running validation.");
}
}
/**
* Adds a new message validator.
*
* @param validatorId
* A string id of the new validator.
* @param messageValidator
* A validator instance.
*/
public void addValidator(final String validatorId, final MessageValidator messageValidator) {
validators.put(validatorId, messageValidator);
}
/**
* Gets the validator with the given id.
*
* @param validatorId
* A string id of the validator.
* @return The validator instance or null if there is no such validator with the given id.
*/
public MessageValidator getValidator(final String validatorId) {
return validators.get(validatorId);
}
/**
* Get all the validators requested in the list of ids.
*
* @param validatorIds
* A list of ids of validators to be returned.
* @return The list of requested validators.
*/
public List<MessageValidator> getValidators(final List<String> validatorIds) {
final List<MessageValidator> _validators = new ArrayList<>();
for (final String id : validatorIds) {
_validators.add(getValidator(id));
}
return _validators;
}
/**
* Starts the validation process. This mainly means starting a new validator thread.
*/
public void startValidation() {
if (validationThread == null || !validationThread.isAlive()) {
expectLastMessage = false;
validationThread = new Thread(new ValidationThread());
validationThread.setDaemon(true); // we do not want to block JVM
validationThread.start();
}
}
/**
* Wait for the validation to be finished. The call is blocked until the validator thread finishes execution or an exception
* is thrown. Internally, this joins the validator thread to the current thread.
*
* @throws InterruptedException
* If the validator thread was interrupted.
*/
public void waitForValidation() throws InterruptedException {
if (validationThread != null) {
fastForward = true;
expectLastMessage = true;
validationThread.join();
}
}
/**
* Interrupts the validator thread immediately. There might be remaining unfinished validations.
*/
public void terminateNow() {
if (validationThread != null) {
validationThread.interrupt();
}
}
/**
* Submits a new validation task. The message response in it will be validated.
*
* @param validationTask
* The new validation task to be processed.
*/
public void submitValidationTask(final ValidationTask validationTask) {
validationTasks.add(validationTask);
}
/**
* Gets the number of messages that needs to be validated.
*
* @return The current size of the file queue with messages waiting for validation.
*/
public int messagesToBeValidated() {
return validationTasks.size();
}
/**
* Is validation facility enabled?
*
* @return <code>true</code> if validation is enabled.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Enables/disables validation. This only takes effect before the validation is started and or finished.
*
* @param enabled
* Specifies whether we want the validation to be enabled.
*/
public void setEnabled(final boolean enabled) {
if (enabled || finished) {
this.enabled = enabled;
} else {
log.error("Validation cannot be disabled while the validation is in progress.");
}
}
/**
* Determines whether the validation process finished already.
*
* @return <code>true</code> if the validation finished or was not started yet.
*/
public boolean isFinished() {
return finished;
}
/**
* Determines whether the validation process is performed in a fast forward mode.
* <p>Unless specified in the scenario, the validation thread has some sleep for it not to influence measurement.
* At the end, when there is nothing else to do, we can go through the remaining responses faster.</p>
*
* <p>The fast forward mode removes the sleep.</p>
*
* @return <code>true</code> if the sleep period is disabled.
*/
public boolean isFastForward() {
return fastForward;
}
/**
* Enables/disables the fast forward mode of the validation.
*
* @param fastForward
* <code>true</code> to enable the fast forward mode.
*/
public void setFastForward(final boolean fastForward) {
this.fastForward = fastForward;
}
public boolean isAllMessagesValid() {
return allMessagesValid;
}
private void logStatistics() {
final StringBuilder sb = new StringBuilder("=== Validation Statistics ===\n");
final Score total = statistics.get(OVERALL_STAT_KEY);
sb.append("= Overall validated ").append(total.getPassed() + total.getFailed()).append(" messages of which ");
sb.append(total.getPassed()).append(" passed and ");
sb.append(total.getFailed()).append(" failed.\n");
for (final Map.Entry<String, Score> entry : statistics.entrySet()) {
final String key = entry.getKey();
final Score value = entry.getValue();
if (!OVERALL_STAT_KEY.equals(key)) {
sb.append("= Thread [").append(key).append("]: Totally validated ").append(value.getFailed() + value.getPassed());
sb.append(" messages of which ").append(value.getPassed()).append(" passed and ").append(value.getFailed()).append(" failed.\n");
}
}
sb.append("=== End of statistics. ===");
log.info(sb.toString());
}
/**
* Gets the overall validation statistics.
*
* @return The overall statistics score.
*/
protected Score getOverallStatistics() {
return statistics.get(OVERALL_STAT_KEY);
}
protected static final class Score {
private long passed = 0;
private long failed = 0;
public long getPassed() {
return passed;
}
public void incPassed() {
passed = passed + 1;
}
public long getFailed() {
return failed;
}
public void incFailed() {
failed = failed + 1;
}
public String toString() {
return String.format("Score: total %d, passed %d, failed %d", passed + failed, passed, failed);
}
}
/**
* Represents the internal validator thread. The thread validates one message with all registered validators and then
* sleeps for 500ms. This is needed for the validation not to influence measurement. After a call to {@link #waitForValidation()} the
* sleeps are skipped.
*/
private class ValidationThread implements Runnable {
@Override
public void run() {
boolean isMessageValid;
ReceivedMessage receivedMessage;
ValidationTask validationTask;
finished = false;
allMessagesValid = true;
if (validators.isEmpty()) {
log.warn("No validators set in scenario.");
return;
}
try {
while (!validationThread.isInterrupted() && (!expectLastMessage || !validationTasks.isEmpty())) {
validationTask = validationTasks.poll();
receivedMessage = null;
if (validationTask != null) {
receivedMessage = validationTask.getReceivedMessage();
for (final MessageValidator validator : getValidators(receivedMessage.getSentMessageTemplate().getValidatorIds())) {
isMessageValid = validator.isValid(receivedMessage.getSentMessage(), new Message(receivedMessage.getResponse()), receivedMessage.getMessageAttributes());
if (log.isTraceEnabled()) {
log.trace(String.format("Message response %s validated with %s returns %s.", receivedMessage.getResponse().toString(), validator.toString(), String.valueOf(isMessageValid)));
}
if (log.isInfoEnabled()) {
if (isMessageValid) {
statistics.get(OVERALL_STAT_KEY).incPassed();
Score score = statistics.get(validationTask.getThreadName());
if (score == null) {
score = new Score();
statistics.put(validationTask.getThreadName(), score);
}
score.incPassed();
} else {
statistics.get(OVERALL_STAT_KEY).incFailed();
Score score = statistics.get(validationTask.getThreadName());
if (score == null) {
score = new Score();
statistics.put(validationTask.getThreadName(), score);
}
score.incFailed();
}
}
allMessagesValid &= isMessageValid;
}
}
if (!fastForward || receivedMessage == null) {
Thread.sleep(500); // we do not want to block senders
}
}
} catch (final InterruptedException ex) {
// never mind, we have been asked to terminate
}
if (log.isInfoEnabled()) {
logStatistics();
log.info("The validator thread finished with the result: " + (allMessagesValid ? "all messages are valid." : "there were validation errors."));
}
finished = true;
}
}
}