convex.core.Result Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of convex-core Show documentation
Show all versions of convex-core Show documentation
Convex core libraries and common utilities
The newest version!
package convex.core;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import convex.core.data.ACell;
import convex.core.data.AHashMap;
import convex.core.data.AMap;
import convex.core.data.ARecordGeneric;
import convex.core.data.AString;
import convex.core.data.AVector;
import convex.core.data.Address;
import convex.core.data.Blob;
import convex.core.data.Format;
import convex.core.data.Keyword;
import convex.core.data.Keywords;
import convex.core.data.Maps;
import convex.core.data.Strings;
import convex.core.data.Tag;
import convex.core.data.Vectors;
import convex.core.data.prim.CVMLong;
import convex.core.exceptions.BadFormatException;
import convex.core.exceptions.InvalidDataException;
import convex.core.exceptions.MissingDataException;
import convex.core.exceptions.ResultException;
import convex.core.lang.Context;
import convex.core.lang.RT;
import convex.core.lang.RecordFormat;
import convex.core.lang.exception.AExceptional;
import convex.core.lang.exception.ErrorValue;
/**
* Class representing the result of a Convex interaction (typically a query or transaction).
*
* A Result is typically used to communicate the outcome of a query or a transaction from a peer to a client.
*
* Contains:
*
* - Message ID - used for message correlation
* - Result value - Any CVM value as the result (may be an error message)
* - Error Code - Error Code, or null if the Result was a success
* - Log Records
* - Additional info
*
*
*
*/
public final class Result extends ARecordGeneric {
private static final RecordFormat RESULT_FORMAT=RecordFormat.of(Keywords.ID,Keywords.RESULT,Keywords.ERROR,Keywords.LOG,Keywords.INFO);
private static final long FIELD_COUNT=RESULT_FORMAT.count();
private static final long ID_POS=RESULT_FORMAT.indexFor(Keywords.ID);
private static final long RESULT_POS=RESULT_FORMAT.indexFor(Keywords.RESULT);
private static final long ERROR_POS=RESULT_FORMAT.indexFor(Keywords.ERROR);
private static final long INFO_POS=RESULT_FORMAT.indexFor(Keywords.INFO);
private static final long LOG_POS=RESULT_FORMAT.indexFor(Keywords.LOG);
// internal value used for empty logs
private static final AVector> EMPTY_LOG = null;
private Result(AVector values) {
super(RESULT_FORMAT, values);
}
public static Result buildFromVector(AVector values) {
return new Result(values);
}
/**
* Create a Result
* @param id ID of Result message
* @param value Result Value
* @param errorCode Error Code (may be null for success)
* @param info Additional info
* @return Result instance
*/
public static Result create(CVMLong id, ACell value, ACell errorCode, AHashMap info) {
return buildFromVector(Vectors.of(id,value,errorCode,EMPTY_LOG,info));
}
/**
* Create a Result
* @param id ID of Result message
* @param value Result Value
* @param errorCode Error Code (may be null for success)
* @param log Log entries created during transaction
* @param info Additional info
* @return Result instance
*/
public static Result create(CVMLong id, ACell value, ACell errorCode, AVector> log,AHashMap info) {
return buildFromVector(Vectors.of(id,value,errorCode,log,info));
}
/**
* Create a Result
* @param id ID of Result message
* @param value Result Value
* @param errorCode Error Code (may be null for success)
* @return Result instance
*/
public static Result create(CVMLong id, ACell value, ACell errorCode) {
return create(id,value,errorCode,null);
}
/**
* Create a Result
* @param id ID of Result message
* @param value Result Value
* @return Result instance
*/
public static Result create(CVMLong id, ACell value) {
return create(id,value,null,null);
}
public static Result error(Keyword errorCode, AString message) {
return error(errorCode,message,null);
}
public static Result error(Keyword errorCode, String message) {
return error(errorCode,Strings.create(message),null);
}
private static Result error(Keyword errorCode, AString message, AHashMap info) {
return create(CVMLong.ZERO,message,errorCode,info);
}
/**
* Returns the message ID for this result. Message ID is an arbitrary ID assigned by a client requesting a transaction.
*
* @return ID from this result
*/
public ACell getID() {
return values.get(ID_POS);
}
/**
* Returns the value for this result. The value is the result of transaction execution (may be an error message if the transaction failed)
*
* @param Type of Value
* @return ID from this result
*/
@SuppressWarnings("unchecked")
public T getValue() {
return (T)values.get(RESULT_POS);
}
/**
* Returns the stack trace for this result. May be null
*
* @return Trace vector from this result
*/
@SuppressWarnings("unchecked")
public AVector getTrace() {
AMap info=getInfo();
if (info instanceof AMap) {
AMap m=(AMap) info;
return (AVector) m.get(Keywords.TRACE);
}
return null;
}
/**
* Returns the info for this Result. May be null
*
* @return Info map from this result
*/
@SuppressWarnings("unchecked")
public AMap getInfo() {
return (AMap) values.get(INFO_POS);
}
/**
* Returns this Result with extra info field
* @param k Information field key
* @param v Information field value
* @return Updated result
*/
public Result withInfo(Keyword k, ACell v) {
AMap info = getInfo();
if (info==null) info=Maps.empty();
AMap newInfo=info.assoc(k, v);
if (newInfo==info) return this;
return new Result(values.assoc(INFO_POS, newInfo));
}
public Result withSource(Keyword source) {
return withInfo(Keywords.SOURCE, source);
}
/**
* Returns the log for this Result. May be an empty vector.
*
* @return Log Vector from this Result
*/
@SuppressWarnings("unchecked")
public AVector> getLog() {
AVector> log=(AVector>) values.get(LOG_POS);
if (log==null) log=Vectors.empty();
return log;
}
/**
* Returns the Error Code from this Result. Normally this should be a Keyword.
*
* Will be null if no error occurred.
*
* @return Error code from this result
*/
public ACell getErrorCode() {
return values.get(ERROR_POS);
}
/**
* Returns the error source code from this Result (see CAD11). This a Keyword.
*
* Will be null if :source info not available
*
* @return Source code keyword from this result,. or null if not present / invalid
*/
public Keyword getSource() {
AMap info = getInfo();
if (info==null) return null;;
ACell source=info.get(Keywords.SOURCE);
if (source instanceof Keyword) return (Keyword)source;
return null;
}
@Override
public AVector values() {
return values;
}
@Override
protected Result withValues(AVector newValues) {
if (values==newValues) return this;
return new Result(newValues);
}
@Override
public void validateCell() throws InvalidDataException {
super.validateCell();
String problem=checkValues(values);
if (problem!=null) {
throw new InvalidDataException(problem, this);
}
}
private static String checkValues(AVector values) {
if (values.count()!=FIELD_COUNT) return "Wrong number of fields for Result";
ACell id=values.get(ID_POS);
if ((id!=null)&&!(id instanceof CVMLong)) {
return "Result ID must be a CVM long value";
}
ACell info=values.get(INFO_POS);
if ((info!=null)&&!(info instanceof AHashMap)) {
return "Result info must be a hash map";
}
ACell log=values.get(LOG_POS);
if ((log!=null)&&!(log instanceof AVector)) {
return "Result log must be a Vector";
}
return null;
}
@Override
public int encode(byte[] bs, int pos) {
bs[pos++]=Tag.RESULT;
pos=values.encodeRaw(bs,pos);
return pos;
}
/**
* Reads a Result from a Blob encoding. Assumes tag byte already checked.
*
* @param b Blob to read from
* @param pos Start position in Blob (location of tag byte)
* @return New decoded instance
* @throws BadFormatException In the event of any encoding error
*/
public static Result read(Blob b, int pos) throws BadFormatException {
int epos=pos;
// include tag location since we are reading raw Vector (will ignore tag)
AVector v=Vectors.read(b,epos);
epos+=Format.getEncodingLength(v);
String problem=checkValues(v);
if (problem!=null) throw new BadFormatException(problem);
Blob enc=v.getEncoding();
Result r=buildFromVector(v);
v.attachEncoding(null); // This is an invalid encoding for vector, see above
r.attachEncoding(enc);
return r;
}
/**
* Tests is the Result represents an Error
* @return True if error, false otherwise
*/
public boolean isError() {
return getErrorCode()!=null;
}
/**
* Constructs a Result from a Context
* @param id Id for Result
* @param rc ResultContext instance from which to extract Result
* @return New Result instance
*/
public static Result fromContext(CVMLong id,ResultContext rc) {
Context ctx=rc.context;
Object result=ctx.getValue();
ACell errorCode=null;
AVector> log = ctx.getLog();
AHashMap info=Maps.empty();
if (result instanceof AExceptional) {
AExceptional ex=(AExceptional)result;
result=ex.getMessage();
errorCode=ex.getCode();
if (ex instanceof ErrorValue) {
ErrorValue ev=(ErrorValue) ex;
AVector> trace=Vectors.create(ev.getTrace());
Address errorAddress=ev.getAddress();
// Maps.of(Keywords.TRACE,trace,Keywords.ADDRESS,address);
info=info.assoc(Keywords.TRACE, trace);
info=info.assoc(Keywords.EADDR, errorAddress);
}
}
if (rc.memUsed>0) info=info.assoc(Keywords.MEM, CVMLong.create(rc.memUsed));
if (rc.totalFees>0) info=info.assoc(Keywords.FEES, CVMLong.create(rc.totalFees));
if (rc.juiceUsed>0) info=info.assoc(Keywords.JUICE, CVMLong.create(rc.juiceUsed));
if (rc.source!=null) info=info.assoc(Keywords.SOURCE, rc.source);
return create(id,(ACell)result,errorCode,log,info);
}
public Result withExtraInfo(Map extInfo) {
if ((extInfo!=null)&&(!extInfo.isEmpty())) {
AMap info=getInfo();
if (info==null) info=Maps.empty();
for (Map.Entry me: extInfo.entrySet()) {
info=info.assoc(me.getKey(), me.getValue());
}
return new Result(values.assoc(INFO_POS, info));
}
return this;
}
/**
* Constructs a Result from a Context. No ResultContext implies we are not in a top level transaction, so minimise work
* @param ctx Context
* @return New Result instance
*/
public static Result fromContext(Context ctx) {
ACell rval=(ctx.isExceptional())?ctx.getExceptional().getMessage():ctx.getResult();
return create(null,rval,ctx.getErrorCode(),null);
}
/**
* Updates result with a given message ID. Used to tag Results for return to Clients
* @param id New Result message ID
* @return Updated Result
*/
public Result withID(ACell id) {
return withValues(values.assoc(ID_POS, id));
}
@Override
public byte getTag() {
return Tag.RESULT;
}
@Override
public RecordFormat getFormat() {
return RESULT_FORMAT;
}
/**
* Constructs a result from a caught exception
* @param e Exception caught
* @return Result instance representing the exception (will be an error)
*/
public static Result fromException(Throwable e) {
if (e==null) return Result.error(ErrorCodes.EXCEPTION,Strings.NIL);
if (e instanceof TimeoutException) {
String msg=e.getMessage();
return Result.error(ErrorCodes.TIMEOUT,Strings.create(msg));
}
if (e instanceof IOException) {
String msg=e.getMessage();
return Result.error(ErrorCodes.IO,Strings.create(msg));
}
if (e instanceof BadFormatException) {
String msg=e.getMessage();
return Result.error(ErrorCodes.FORMAT,Strings.create(msg));
}
if (e instanceof MissingDataException) {
return MISSING_RESULT;
}
if (e instanceof ResultException) {
return ((ResultException) e).getResult();
}
if ((e instanceof ExecutionException)||(e instanceof CompletionException)) {
// use the underlying cause
return fromException(e.getCause());
}
if (e instanceof InterruptedException) {
// This is special, we need to ensure the interrupt status is set
return interruptThread();
}
return Result.error(ErrorCodes.EXCEPTION,Strings.create(e.getMessage()));
}
// Standard result in case of interrupts
// Note interrupts are always caused by CLIENT from a local perspective
private static final Result INTERRUPTED_RESULT=Result.error(ErrorCodes.INTERRUPTED,Strings.create("Interrupted!")).withSource(SourceCodes.CLIENT);
private static final Result MISSING_RESULT=Result.error(ErrorCodes.MISSING,Strings.create("Missing Data!")).withSource(SourceCodes.CLIENT);
/**
* Returns a Result representing a thread interrupt, AND sets the interrupt status on the current thread
* @return Result instance representing an interruption
*/
public static Result interruptThread() {
Thread.currentThread().interrupt();
return INTERRUPTED_RESULT;
}
/**
* Converts this result to a JSON representation. WARNING: some information may be lost because JSON is a terrible format.
*
* @return JSON Object containing values from this Result
*/
public HashMap toJSON() {
HashMap hm=new HashMap<>();
if (isError()) {
hm.put("errorCode", RT.name(getErrorCode()).toString());
}
hm.put("value", RT.json(getValue()));
AVector> log = getLog();
if (log!=null) hm.put("info", RT.json(log));
AMap info = getInfo();
if (info!=null) hm.put("info", RT.json(info));
return hm;
}
}