/*
* Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
* See LICENSE in the project root for license information.
*/
package com.linkedin.flashback;
import com.google.common.collect.Iterables;
import com.linkedin.flashback.matchrules.DummyMatchRule;
import com.linkedin.flashback.matchrules.MatchRule;
import com.linkedin.flashback.scene.DummyScene;
import com.linkedin.flashback.scene.Scene;
import com.linkedin.flashback.serializable.RecordedHttpExchange;
import com.linkedin.flashback.serializable.RecordedHttpRequest;
import com.linkedin.flashback.serializable.RecordedHttpResponse;
import com.linkedin.flashback.serialization.SceneWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Provide functionality to read, write and lookup RecordedHttpExchange to scene.
* Also, it allows client to change scene at running time.
*
* @author shfeng
* @author dvinegra
*/
public class SceneAccessLayer {
static final String THE_SCENE_IS_NOT_READABLE = "the scene is not readable";
static final String SCENE_IS_NOT_ALLOWED_BE_NULL = "scene is not allowed to be null";
static final String MATCHRULE_IS_NOT_ALLOWED_BE_NULL = "matchrule is not allowed to be null";
static final String SCENEWRITER_IS_NOT_ALLOWED_BE_NULL = "scenewriter is not allowed to be null";
static final String NO_MATCHING_RECORDING_FOUND = "no matching recording found";
static final String FAILED_TO_WRITE_SCENE_TO_THE_FILE = "Failed to write scene to the file";
private SceneWriter _sceneWriter;
private Scene _scene;
private MatchRule _matchRule;
private int _sequencePosition = 0;
private boolean _dirty = false;
public SceneAccessLayer(Scene scene, SceneWriter sceneWriter, MatchRule matchRule) {
if (scene == null) {
throw new IllegalArgumentException(SCENE_IS_NOT_ALLOWED_BE_NULL);
}
if (sceneWriter == null) {
throw new IllegalArgumentException(SCENEWRITER_IS_NOT_ALLOWED_BE_NULL);
}
if (matchRule == null) {
throw new IllegalArgumentException(MATCHRULE_IS_NOT_ALLOWED_BE_NULL);
}
_sceneWriter = sceneWriter;
_matchRule = matchRule;
_scene = scene;
}
public SceneAccessLayer(Scene scene, MatchRule matchRule) {
this(scene, new SceneWriter(), matchRule);
}
public SceneAccessLayer() {
this(new DummyScene(), new SceneWriter(), new DummyMatchRule());
}
public String getSceneName() {
return _scene.getName();
}
/**
* set match rule
* */
public void setMatchRule(MatchRule matchRule) {
if (matchRule == null) {
throw new IllegalArgumentException(MATCHRULE_IS_NOT_ALLOWED_BE_NULL);
}
_matchRule = matchRule;
}
/**
* set scene if client need use switch scenes at run time.
*
* */
public void setScene(Scene scene) {
if (scene == null) {
throw new IllegalArgumentException(SCENE_IS_NOT_ALLOWED_BE_NULL);
}
flush();
_scene = scene;
_sequencePosition = 0;
}
public boolean canPlayback() {
return _scene.isReadable();
}
/**
* Check if incoming http request matches any HttpExchange from the scene
* @param request incoming request from client
* @return true if found matched request, otherwise return false
*
* */
public boolean hasMatchRequest(RecordedHttpRequest request) {
return findMatchRequest(request) >= 0;
}
/**
* Given incoming http request, find matched response from the scene and return response from the scene
* @param request http request from client
* @return matched http response from the scene
*
* */
public RecordedHttpResponse playback(RecordedHttpRequest request) {
if (!_scene.isReadable()) {
throw new IllegalStateException(THE_SCENE_IS_NOT_READABLE);
}
int position = findMatchRequest(request);
if (position < 0) {
throw new IllegalStateException(NO_MATCHING_RECORDING_FOUND);
}
if (_scene.isSequential()) {
_sequencePosition++;
}
List<RecordedHttpExchange> recordedHttpExchangeList = _scene.getRecordedHttpExchangeList();
return recordedHttpExchangeList.get(position).getRecordedHttpResponse();
}
/**
* Record request and response to the scene. Updates will be performed in-memory and will be written to disk
* when flush() is called, or when the Scene is changed.
* @param recordedHttpRequest http request from client
* @param recordedHttpResponse http response from upstream service
*
* */
public void record(RecordedHttpRequest recordedHttpRequest, RecordedHttpResponse recordedHttpResponse) {
List<RecordedHttpExchange> recordedHttpExchangeList = _scene.getRecordedHttpExchangeList();
RecordedHttpExchange recordedHttpExchange =
new RecordedHttpExchange(recordedHttpRequest, recordedHttpResponse, new Date());
if (!_scene.isSequential()) {
int position = findMatchRequest(recordedHttpRequest);
if (position >= 0) {
recordedHttpExchangeList.set(position, recordedHttpExchange);
} else {
recordedHttpExchangeList.add(recordedHttpExchange);
}
} else {
recordedHttpExchangeList.add(recordedHttpExchange);
}
_dirty = true;
}
/**
* Serialize the scene to disk, if it has been updated
*/
public void flush() {
if (_dirty) {
try {
_sceneWriter.writeScene(_scene);
_dirty = false;
} catch (IOException e) {
throw new RuntimeException(FAILED_TO_WRITE_SCENE_TO_THE_FILE, e);
}
}
}
/**
* produces a string description for the match failure reason for a particular request
* @param request incoming request that we are trying to match
* @return a String describing the match failure reasons for the request
*/
public String getMatchFailureDescription(RecordedHttpRequest request) {
List<String> failureDescriptionList = new ArrayList<>();
List<RecordedHttpExchange> exchangeList = _scene.getRecordedHttpExchangeList();
if (_scene.isSequential()) {
if (_sequencePosition < exchangeList.size()) {
failureDescriptionList.add(_matchRule.getMatchFailureDescriptionForRequests(request, exchangeList.get(_sequencePosition).getRecordedHttpRequest()));
} else {
failureDescriptionList.add("No more recorded requests in sequential scene");
}
} else {
for (int i = 0; i < exchangeList.size(); i++) {
RecordedHttpExchange exchange = exchangeList.get(i);
failureDescriptionList.add(String.format("Recorded Request %d:%n%s", i + 1,
_matchRule.getMatchFailureDescriptionForRequests(request, exchange.getRecordedHttpRequest())));
}
}
return new StringBuilder().append("Could not find matching request in scene " + _scene.getName() + "%n")
.append(String.join("%n", failureDescriptionList))
.toString();
}
/**
* find matched request from scene
* @param request incoming request that we'd like match in existing scene
* @return position of list of HttpExchanges from the scene. return -1 if no match found
*
* */
private int findMatchRequest(final RecordedHttpRequest request) {
if (_scene.isSequential()) {
List<RecordedHttpExchange> exchangeList = _scene.getRecordedHttpExchangeList();
// In sequential playback mode, only test the request at the current sequence index
if (_sequencePosition < exchangeList.size() && _matchRule
.test(request, exchangeList.get(_sequencePosition).getRecordedHttpRequest())) {
return _sequencePosition;
}
return -1;
} else {
return Iterables.indexOf(_scene.getRecordedHttpExchangeList(),
input -> _matchRule.test(request, input.getRecordedHttpRequest()));
}
}
}