org.anystub.Base Maven / Gradle / Ivy
Show all versions of anystub Show documentation
package org.anystub;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Collections.singletonList;
import static org.anystub.RequestMode.rmAll;
import static org.anystub.RequestMode.rmNew;
import static org.anystub.RequestMode.rmNone;
import static org.anystub.RequestMode.rmPassThrough;
import static org.anystub.RequestMode.rmTrack;
/**
* provides basic access to stub-file
*
* methods put/get* allow work with in-memory cache
* methods request* allow get/keep data in file
*
* Check {@link RequestMode} to find options to control get access to external system and store requests strategy
*
*/
public class Base {
private static Logger log = Logger.getLogger(Base.class.getName());
private List documentList = new ArrayList<>();
private Iterator documentListTrackIterator;
private List requestHistory = new ArrayList<>();
private final String filePath;
private boolean isNew = true;
private RequestMode requestMode = rmNew;
private final PropertyContainer propertyContainer = new PropertyContainer();
/**
* creates stub by specific path.
* in your test you do not need to create it directly. Use org.anystub.mgmt.BaseManagerFactory.getStub()
* to get stub related to your context
*
*
* Note: Consider using instead
*
* @param path the path to stub file if filename holds only filename (without path) then creates file in src/test/resources/anystub/
* examples: new Base("./stub.yml") uses/creates file in current/work dir, new Base("stub.yml") uses/creates src/test/resources/anystub/stub.yml;
*/
public Base(String path) {
this.filePath = path;
}
/**
* set constrains for using cache and getting access a source system
*
* @param requestMode {@link RequestMode}
* @return this to cascade operations
*/
public Base constrain(RequestMode requestMode) {
if (isNew()) {
this.requestMode = requestMode;
switch (requestMode) {
case rmNone:
init();
break;
case rmAll:
isNew = false;
break;
case rmTrack:
init();
if (documentList.isEmpty()) {
documentListTrackIterator = null;
} else {
documentListTrackIterator = documentList.iterator();
}
break;
default:
break;
}
} else if (this.requestMode != requestMode) {
log.warning(() -> String.format("Stub constrains change after creation for %s. Consider to split stub-files", filePath));
this.requestMode = requestMode;
}
return this;
}
/**
* Keeps a document in cache.
* initialize cache
*
* @param document for keeping
* @return inserted document
*/
public Document put(Document document) {
documentList.add(document);
isNew = false;
return document;
}
/**
* Creates and keeps a new Document in cache.
* treats to keysAndValue[0..count-1] as keys of new Document, the last element as the value of the Document
*
* @param keysAndValue keys for request2
* @return new Document
*/
public Document put(String... keysAndValue) {
return put(Document.fromArray(keysAndValue));
}
/**
* Creates and keeps a new Document in cache.
* Document includes request and exception as a response.
*
* @param ex exception is kept in document
* @param keys key for the document
* @return inserted document
*/
public Document put(Throwable ex, String... keys) {
return put(new Document(ex, keys));
}
/**
* Finds document with given keys.
* if document is found then it returns an Optional containing the first value from the response.
* If document is not found then it returns empty Optional.
* If found document contains an exception the exception will be
* raised.
*
* @param keys for search of the document
* @return first value from document's response or empty
*/
public Optional getOpt(String... keys) {
return documentList.stream()
.filter(x -> x.keyEqual_to(keys))
.map(Document::get)
.findFirst();
}
public String get(String... keys) {
return getVals(keys).iterator().next();
}
/**
* Finds document with the given key. If document found then returns iterator to the values from the document
*
* @param keys for search document
* @return values of requested document
* @throws NoSuchElementException throws when document is not found
*/
public Iterable getVals(String... keys) throws NoSuchElementException {
return getDocument(keys)
.orElseThrow(NoSuchElementException::new)
.getVals();
}
private Optional getDocument(String... keys) {
return documentList.stream()
.filter(x -> x.keyEqual_to(keys))
.findFirst();
}
/**
* Requests a string from stub.
* If this document is absent in cache throws {@link NoSuchElementException}
*
* @param keys keys for searching response in a stub-file
* @return requested response
* @throws NoSuchElementException if document if not found in cache
*/
public String request(String... keys) throws NoSuchElementException {
return request(Base::throwNSE,
values -> values,
Base::throwNSE,
keys);
}
/**
* Requests a string. It looks for a Document in a stub-file
* If it is not found then requests the value from the supplier.
* supplier could request the string from an external system.
*
* @param supplier method to obtain response
* @param keys keys for document and parameters for request real system
* @param some
* @return response from real system
* @throws E type of expected exception
*/
public String request(Supplier supplier, String... keys) throws E {
return request(supplier,
values -> values,
s -> s,
keys);
}
/**
* Requests Boolean
*
* @param supplier
* @param keys
* @param
* @return
* @throws E
*/
public Boolean requestB(Supplier supplier, String... keys) throws E {
return request(supplier,
Boolean::parseBoolean,
String::valueOf,
keys);
}
/**
* requests Integer
*
* @param supplier
* @param keys
* @param
* @return
* @throws E
*/
public Integer requestI(Supplier supplier, String... keys) throws E {
return request(supplier,
Integer::parseInt,
String::valueOf,
keys);
}
/**
* Requests serializable object
*
* @param supplier provides requested object
* @param keys keys for document and parameters for request real system
* @param expected type for requested object
* @param expected exception
* @return recovered object
* @throws E expected exception
*/
public T requestSerializable(Supplier supplier, String... keys) throws E {
return request(supplier,
Util::decode,
Util::encode,
keys);
}
/**
* Requests an array of string from stub.
* If this document is absent in cache throws {@link NoSuchElementException}
*
* @param keys keys for searching response in stub
* @param type of allowed Exception
* @return requested response
* @throws E if document if not found in cache
*/
public String[] requestArray(String... keys) throws E {
return request2(Base::throwNSE,
values -> values == null ? null : StreamSupport.stream(values.spliterator(), false).collect(Collectors.toList()).toArray(new String[0]),
Base::throwNSE,
keys);
}
/**
* Requests an array of string.
*
* @param supplier provide string array from system
* @param keys keys for request
* @param expected exception
* @return string array. it could be null;
* @throws E expected exception
*/
public String[] requestArray(Supplier supplier, String... keys) throws E {
return request2(supplier,
values -> values == null ? null : StreamSupport.stream(values.spliterator(), false).collect(Collectors.toList()).toArray(new String[0]),
Arrays::asList,
keys);
}
/**
* Requests an Object from stub-file.
* If Document is found uses {@link DecoderSimple} to build result. It could build object of any class
* If this document is absent in cache throws {@link NoSuchElementException}
*
* @param decoder recover object from strings
* @param keys key for creating request
* @param type of requested object
* @param type of thrown Exception by {@link java.util.function.Supplier}
* @return requested object
* @throws E thrown Exception by {@link java.util.function.Supplier}
*/
public T request(DecoderSimple decoder,
String... keys) throws E {
return request2(Base::throwNSE,
values -> values == null ? null : decoder.decode(values.iterator().next()),
null,
keys
);
}
/**
* Requests an Object which is could be kept in stub-file as a single string
*
* @param supplier provide real answer
* @param decoder create object from one line
* @param encoder serialize object to one line
* @param keys key of object
* @param Type of Object
* @param thrown exception by supplier
* @return result from recovering from stub or from supplier
* @throws E exception from stub or from supplier
*/
public T request(Supplier supplier,
DecoderSimple decoder,
EncoderSimple encoder,
String... keys) throws E {
return request2(supplier,
values -> values == null ? null : decoder.decode(values.iterator().next()),
t -> t == null ? null : singletonList(encoder.encode(t)),
keys
);
}
/**
* Looks for an Object in stub-file or gets it from the supplier. Uses encoder and decoder to convert the request
* and results in the stub-file. Uses keys to match the request in the stub-files
*
* @param supplier provide real answer
* @param decoder create object from values
* @param encoder serialize object
* @param keys key of object
* @param Type of Object
* @param thrown exception by supplier
* @return result from recovering from stub or from supplier, it could return null if it gets null from upstream and decoded
* @throws E exception from stub or from supplier
*/
public T request2(Supplier supplier,
Decoder decoder,
Encoder encoder,
String... keys) throws E {
return request2(supplier,
decoder,
encoder,
() -> keys);
}
/**
* Looks for an Object in stub-file or gets it from the supplier. Uses encoder and decoder to convert the request
* and results in the stub-file. Uses keysGen to get keys to match the request in the stub-files
*
* @param supplier - provides the value from an external system
* @param decoder - recovers result from stub
* @param encoder - converts result to strings for stub-file
* @param keyGen - provides keys to match requested document
* @param - type of requested object
* @param - allowed exception
* @return an object from stub or an external system
* @throws E generates the exception if an external system generated it
*/
public T request2(Supplier supplier,
Decoder decoder,
Encoder encoder,
KeysSupplier keyGen) throws E {
if (requestMode == rmPassThrough) {
return supplier.get();
}
KeysSupplier keyGenCashed = new KeysSupplierCashed(keyGen);
log.finest(() -> String.format("request executing: %s", String.join(",", keyGenCashed.get())));
if (isNew()) {
init();
}
if (seekInCache()) {
Optional storedDocument = getDocument(keyGenCashed.get());
if (storedDocument.isPresent()) {
requestHistory.add(storedDocument.get());
if (storedDocument.get().isNullValue()) {
// it's not necessarily to decode null objects
return null;
}
return decoder.decode(storedDocument.get().getVals());
}
} else if (isTrackCache()) {
if (documentListTrackIterator.hasNext()) {
Document next = documentListTrackIterator.next();
if (next.keyEqual_to(keyGenCashed.get())) {
requestHistory.add(next);
return decoder.decode(next.getVals());
}
}
}
if (!writeInCache()) {
throwNSE(Arrays.toString(keyGenCashed.get()));
}
// execute
// it could raise any exception so need to catch Throwable
T res;
try {
res = supplier.get();
} catch (Throwable ex) {
Document exceptionalDocument = put(ex, keyGenCashed.get());
requestHistory.add(exceptionalDocument);
try {
save();
} catch (IOException ioEx) {
log.warning(() -> "exception information is not saved into stub: " + ioEx);
}
throw ex;
}
// store values
Document retrievedDocument;
Iterable responseData;
if (res == null) {
responseData = null;
retrievedDocument = new Document(keyGenCashed.get());
} else {
responseData = encoder.encode(res);
ArrayList values = new ArrayList<>();
for (String responseDatum : responseData) {
values.add(responseDatum);
}
retrievedDocument = new Document(keyGenCashed.get(), values.toArray(new String[0]));
}
put(retrievedDocument);
requestHistory.add(retrievedDocument);
try {
save();
} catch (IOException ex) {
log.warning(() -> "exception information is not saved into stub: " + ex);
}
if (responseData == null) {
return null;
}
return decoder.decode(responseData);
}
/**
* reloads stub-file - IOException exceptions are suppressed
*/
private void init() {
try {
load();
} catch (IOException e) {
log.warning(() -> "loading failed: " + e);
}
}
/**
* cleans history, reloads stub-file
*
* @throws IOException due to file access error
*/
private void load() throws IOException {
File file = new File(filePath);
try (InputStream input = new FileInputStream(file)) {
Yaml yaml = new Yaml(new SafeConstructor());
Object load = yaml.load(input);
if (load instanceof Map) {
clear();
Map map = (Map) load;
map.forEach((k, v) -> documentList
.add(new Document((Map) v)));
isNew = false;
}
} catch (FileNotFoundException e) {
log.info(() -> String.format("stub file %s is not found: %s", file.getAbsolutePath(), e));
}
}
/**
* rewrites stub-file
*
* @throws IOException due to file access error
*/
public void save() throws IOException {
File file = new File(filePath);
File path = file.getParentFile();
if (path != null && !path.exists()) {
if (path.mkdirs())
log.info(() -> "dirs created");
else
throw new IOException("dirs for stub isn't created");
}
if (!file.exists()) {
if (file.createNewFile())
log.info(() -> "stub file is created:" + file.getAbsolutePath());
else
throw new IOException("stub file isn't created");
}
try (FileWriter output = new FileWriter(file)) {
Yaml yaml = new Yaml(new SafeConstructor());
Map saveList = new LinkedHashMap<>();
for (int i = 0; i < documentList.size(); i++) {
saveList.put(String.format("request%d", i), documentList.get(i).toMap());
}
yaml.dump(saveList, output);
}
}
/**
* if previous load() is successful then isNew returns false
*
* @return true if the stub-file is not load in memory
*/
public boolean isNew() {
return isNew;
}
/**
* clears buffer, set isNew to true
* doesn't touch appropriate file (a note: just remove a file manually if you do not need the data anymore)
* doesn't clean properties
*/
public void clear() {
documentList.clear();
requestHistory.clear();
isNew = true;
}
/**
* equal to: throw new NoSuchElementException(e.toString());
*
* @param e nothing
* @param nothing
* @param nothing
* @return nothing
*/
public static T throwNSE(E e) {
throw new NoSuchElementException(e.toString());
}
/**
* throw new NoSuchElementException(e.toString());
*
* @param type for matching
* @return nothing
*/
public static T throwNSE() throws NoSuchElementException {
throw new NoSuchElementException();
}
/**
* @return stream of all requests
*/
public Stream history() {
return requestHistory.stream();
}
/**
* requests that exa
*
* @param keys keys for searching requests (exactly matching)
* @return stream of requests
*/
public Stream history(String... keys) {
return history()
.filter(x -> x.keyEqual_to(keys));
}
/**
* finds requests in the stub-file by keys keys
* * if no keys provided then it returns all requests.
* * one or more of the keys could be null. That means the matching by the key is omitted.
* * match(null) and match(null,null) are different, match(null) searches requests with at least one string as the key
* match(null, null) looks requests with at least two strings as the key
*
* @param keys keys for matching requests
* @return stream of matched requests
*/
public Stream match(String... keys) {
if (keys == null || keys.length == 0) {
return history();
}
return history()
.filter(x -> x.match_to(keys));
}
/**
* finds requests in the stub-file by keys keys. the same as {#match } but matches each string in the key using regex
*
* @param keys keys for matching
* @return stream of matched documents from history
*/
public Stream matchEx(String... keys) {
if (keys == null || keys.length == 0) {
return history();
}
return history()
.filter(x -> x.matchEx_to(keys));
}
/**
* finds requests in the stub-file, the same as {#matchEx} but uses keys and result fields to match
*
* @param keys keys for matching
* @param values keys for matching
* @return stream of matched documents from history
*/
public Stream matchEx(String[] keys, String[] values) {
return history()
.filter(x -> x.matchEx_to(keys, values));
}
/**
* number of requests with given keys
* * if no keys provided then number of all requests.
* * key could be skipped if you set correspondent value to null.
* * times(null) and times(null,null) are different, cause looking for requests with
* amount of keys no less then in keys array.
*
* @param keys keys for matching requests
* @return amount of matched requests
*/
public long times(final String... keys) {
return match(keys)
.count();
}
/**
* number of requests with given keys
* * if no keys provided then number of all requests.
* * key could be skipped if you set correspondent value to null.
* * times(null) and times(null,null) are different, cause looking for requests with
* amount of keys no less then in keys array.
*
* @param keys keys for matching requests
* @return amount of matched requests
*/
public long timesEx(final String... keys) {
return matchEx(keys)
.count();
}
/**
* number of requests with given keys
* * if no keys then amount of all requests.
* * key could be skipped if you set correspondent value to null.
* * times(null) and times(null,null) are different, cause looking for requests with
* amount of keys no less then in keys array.
*
* @param keys values for matching requests by keys
* @param values values for matching requests by value
* @return amount of matched requests
*/
public long timesEx(final String[] keys, final String[] values) {
return matchEx(keys, values)
.count();
}
public String getFilePath() {
return filePath;
}
private boolean seekInCache() {
return requestMode == rmNew || requestMode == rmNone;
}
private boolean writeInCache() {
return requestMode == rmNew ||
requestMode == rmAll ||
(requestMode == rmTrack && documentListTrackIterator == null);
}
private boolean isTrackCache() {
return requestMode == rmTrack && documentListTrackIterator != null;
}
}