/* * -----------------------------------------------------------------------\ * 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.message.Message; import org.perfcake.util.properties.MandatoryProperty; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Properties; /** * Creates a dictionary of valid responses and use this to validate them in another run. * It is also possible to create the dictionary manually, however, this is too complicated task and we always * recommend running the validation in record mode first. Any manual changes can be done later. * Dictionary validator creates an index file and a separate file for each response. A writable directory must * be specified. The default index file name can be redefined. The response file names are based on hash codes of * the original messages. Empty, null or equal messages will overwrite the file but this is not the intended use * of this validator. Index file is never overwritten, if you really insist on recreating it, please rename or * delete the file manually (this is for safety reasons). * It is not sufficient to store just the index as it is likely that the correct messages will be manually * modified after they are recorded. * * @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a> */ public class DictionaryValidator implements MessageValidator { /** * A logger for this class. */ private final Logger log = LogManager.getLogger(ValidationManager.class); /** * The directory where the dictionary is/will be store. */ @MandatoryProperty private String dictionaryDirectory; /** * The file name of the dictionary index. */ private String dictionaryIndex = "index"; /** * True when the the record mode active. */ private boolean record = false; /** * Did we check the existence of the directory index? We never ever allow its overwrite in the record mode. */ private boolean indexChecked = false; /** * Cached directory index. */ private Properties indexCache; /** * Escapes characters <code>=</code> and <code>:</code> in the payload string. * * @param payload * The payload string to be escaped. * @return Escaped payload. */ private String escapePayload(final String payload) { return payload.replaceAll("[\\n\\r\\\\:= ]", "_"); } /** * Records the correct response. * * @param originalMessage * The original message. * @param response * The response that is considered correct. * @throws ValidationException * If any of the disk operations fails. */ private void recordResponse(final Message originalMessage, final Message response) throws ValidationException { final String responseHashCode = Integer.toString(response.getPayload().toString().hashCode()); final File targetFile = new File(dictionaryDirectory, responseHashCode); if (targetFile.exists()) { throw new ValidationException(String.format("Target file for the message hash code '%s' already exists. Probably a duplicate original message.", responseHashCode)); } try (final Writer indexWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(getIndexFile(), true), StandardCharsets.UTF_8)); final Writer responseWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(targetFile), StandardCharsets.UTF_8))) { indexWriter.append(escapePayload(originalMessage.getPayload().toString())); indexWriter.append("="); indexWriter.append(responseHashCode); indexWriter.append("\n"); responseWriter.write(response.getPayload().toString()); } catch (final IOException e) { throw new ValidationException(String.format("Cannot record correct response for message '%s': ", response.getPayload().toString()), e); } } /** * Reads the index into memory, or returns the previously read index. * * @return The response index. * @throws ValidationException * If any of the disk operations fails. */ private Properties getIndexCache() throws ValidationException { if (indexCache == null) { indexCache = new Properties(); try (final Reader indexReader = new BufferedReader(new InputStreamReader(new FileInputStream(getIndexFile()), StandardCharsets.UTF_8))) { indexCache.load(indexReader); } catch (final IOException e) { throw new ValidationException(String.format("Unable to load index file '%s': ", getIndexFile().getAbsolutePath()), e); } } return indexCache; } /** * Validates the response against the previously recorded correct responses. * * @param originalMessage * The original message. * @param response * The response to be validated. * @return True if and only if the validation passed. * @throws ValidationException * If any of the disk operations fails. */ private boolean validateResponse(final Message originalMessage, final Message response) throws ValidationException { final String responseHashCode = getIndexCache().getProperty(escapePayload(originalMessage.getPayload().toString())); if (responseHashCode == null) { // we do not have any such message return false; } try { final String newResponse = response != null && response.getPayload() != null ? response.getPayload().toString() : ""; final String responseString = new String(Files.readAllBytes(Paths.get(dictionaryDirectory, responseHashCode)), StandardCharsets.UTF_8); return newResponse.equals(responseString); } catch (final IOException e) { throw new ValidationException(String.format("Cannot read correct response from file '%s': ", new File(dictionaryDirectory, responseHashCode).getAbsolutePath()), e); } } /** * Gets the file with index of recorded responses. * * @return The index file. */ private File getIndexFile() { return new File(dictionaryDirectory, dictionaryIndex); } /** * Checks whether the index file exists. * * @return <code>true</code> if and only if the index file exists. */ private boolean indexExists() { return (dictionaryDirectory != null && dictionaryIndex != null) && getIndexFile().exists(); } @Override public boolean isValid(final Message originalMessage, final Message response, final Properties messageAttributes) { if (!indexChecked && record && indexExists()) { // We are in record mode and did not previously check for the index existence. Once this is checked and the index is not present, // we never appear here again. If the check did not pass, we will log the error again and again. log.error("Error while trying to record responses - index file already exists, overwrite not permitted."); return false; } else { indexChecked = true; if (record) { try { // in record mode record the answer (considered as correct) recordResponse(originalMessage, response); return true; } catch (final ValidationException e) { log.error("Error recording correct response: ", e); } } else { try { // in normal mode, validate the answer against already recorded responses return validateResponse(originalMessage, response); } catch (final ValidationException e) { log.error("Error validating response: ", e); } } } return false; } /** * Gets the dictionary directory name. * * @return The dictionary directory name. */ public String getDictionaryDirectory() { return dictionaryDirectory; } /** * Sets Gets the dictionary directory name. * * @param dictionaryDirectory * The name of the dictionary directory. * @return Instance of this for fluent API. */ public DictionaryValidator setDictionaryDirectory(final String dictionaryDirectory) { this.dictionaryDirectory = dictionaryDirectory; return this; } /** * Gets the file name of the dictionary index. * * @return The file name of the dictionary index. */ public String getDictionaryIndex() { return dictionaryIndex; } /** * Sets the file name of the dictionary index. * * @param dictionaryIndex * The file name of the dictionary index. * @return Instance of this for fluent API. */ public DictionaryValidator setDictionaryIndex(final String dictionaryIndex) { this.dictionaryIndex = dictionaryIndex; return this; } /** * Checks whether we are in the record mode. * * @return True if and only if the record mode is active. */ public boolean isRecord() { return record; } /** * Sets the record mode. * * @param record * <code>true</code> to activate the record mode. * @return Instance of this for fluent API. */ public DictionaryValidator setRecord(final boolean record) { this.record = record; return this; } }