/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.lang.op;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import divconq.bus.Message;
import divconq.hub.DomainInfo;
import divconq.hub.Hub;
import divconq.locale.ILocaleResource;
import divconq.locale.ITranslationAdapter;
import divconq.locale.LocaleDefinition;
import divconq.log.DebugLevel;
import divconq.log.HubLog;
import divconq.schema.SchemaManager;
import divconq.session.Session;
import divconq.struct.FieldStruct;
import divconq.struct.ListStruct;
import divconq.struct.RecordStruct;
import divconq.struct.Struct;
import divconq.util.StringUtil;
import divconq.util.TimeUtil;
import divconq.work.TaskRun;
import divconq.xml.XElement;
/**
* Almost all code that executes after Hub.start should have a context. The context
* tells the code who the user responsible for the task is, what their access levels
* are (at a high level), what language/locale/chronology(timezone) they use, how to log the
* debug messages for the task, and whether or not the user has been authenticated or not.
*
* Although the task context is associated with the current thread, it is the task that the
* context belongs to, not the thread. If a task splits into multiple threads there is still
* one TaskContext, even if the task makes a remote call on DivConq's bus that remote call
* executes on the TaskContext.
*
* As long as you use the built-in features - work pool, scheduler, bus, database - the task context
* will smoothly come along with no effort from the app developer.
*
* A quick guide to what context to use where:
*
* Hub Context - useHubContext()
*
* The code is running as part of the Hub core.
*
* Root Context - useNewRoot()
*
* Root context is the same identity as Hub, but with useNewRoot() you get a new log id.
* Use this with code running batch tasks that belong to the system rather than a specific
* user.
*
* Guest Context - useNewGuest()
*
* Guest context is for use by an anonymous user. For example a user through the interchange
* (HTTP, FTP, SFTP, EDI, etc).
*
* User Context - new TaskContext + set(tc)
*
* When a user signs-in create and set a new context. No need to authenticate against the database
* that will happen automatically (as long as you follow DivConq development guidelines) so think
* of creating the user Task Context as information gathering not authentication.
*
* @author Andy
*
*/
public class OperationContext implements ITranslationAdapter {
static protected String runid = null;
static protected String hubid = "00001";
static protected OperationContext hubcontext = null;
static protected OperationContext defaultcontext = null;
static protected ThreadLocal<OperationContext> context = new ThreadLocal<OperationContext>();
static protected AtomicLong nextid = new AtomicLong();
static {
OperationContext.context.set(new OperationContext());
OperationContext.runid = TimeUtil.stampFmt.print(new DateTime(DateTimeZone.UTC));
OperationContext.hubcontext = OperationContext.useNewRoot();
OperationContext.defaultcontext = OperationContext.useNewGuest();
OperationContext.context.set(OperationContext.hubcontext);
}
static public String getHubId() {
return OperationContext.hubid;
}
static public void setHubId(String v) {
if (StringUtil.isEmpty(v))
return;
// only set once
if (v.equals(OperationContext.hubid))
return;
OperationContext.hubid = v;
OperationContext.hubcontext = OperationContext.useNewRoot(); // reset the hub context
OperationContext.defaultcontext = OperationContext.useNewGuest();
OperationContext.context.set(OperationContext.hubcontext);
}
static public String getRunId() {
return OperationContext.runid;
}
/**
* @return the hub context, use by code running in the "core" code of Hub (e.g. main thread)
*/
static public OperationContext getHubContext() {
return OperationContext.hubcontext;
}
/**
* Sets the current thread to use the hub context
*
* @return the hub context, use by code running in the "core" code of Hub (e.g. main thread)
*/
static public OperationContext useHubContext() {
OperationContext.context.set(OperationContext.hubcontext);
return OperationContext.hubcontext;
}
public static void startHubContext(XElement config) {
// TODO load up info from config - this is the only time TC or UC
// should be mutated, only internally and only the special root
// instances
OperationContext.updateHubContext();
}
public static void updateHubContext() {
// this is the only time TC or UC should be mutated, only internally
// and only the special root instances
OperationContext.hubcontext.level = HubLog.getGlobalLevel();
//OperationContext.hubcontext.userctx.context.setField("Locale", Logger.getLocale());
}
// make sure messages size never gets too large, since server could run for months
// without a reset - this could throw off the markers in any OR pointing to hub/default
// but generally that is such a small problem...not worried about it.
// Hub startup and shutdown are exempt from this, which is when they are used most
/*
public static void cleanUp() {
while (OperationContext.hubcontext.messages.size() > 1000)
OperationContext.hubcontext.messages.remove(0);
while (OperationContext.defaultcontext.messages.size() > 1000)
OperationContext.defaultcontext.messages.remove(0);
}
*/
/**
* @return context of the current thread, if any
* otherwise the guest context
*/
static public OperationContext get() {
OperationContext tc = OperationContext.context.get();
if (tc == null) {
// TODO someday monitor how often/where this happens
//System.out.println("someplace without a context");
tc = OperationContext.defaultcontext;
}
return tc;
}
// does the current thread have a context?
static public boolean hasContext() {
return (OperationContext.context.get() != null);
}
/**
* @param v context for current thread to use
*/
static public void set(OperationContext v) {
if (v != null)
OperationContext.context.set(v);
}
/**
* @return context of the current thread, if any, otherwise the hub context
*/
static public OperationContext getOrHub() {
OperationContext tc = OperationContext.context.get();
if (tc == null)
tc = OperationContext.hubcontext;
return tc;
}
/**
* @return create a new guest context
*/
static public OperationContext allocateGuest() {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContextBuilder().withGuestTaskTemplate().toOperationContext();
}
/**
* Sets the current thread to use a new guest context
*
* @return create a new guest context
*/
static public OperationContext useNewGuest() {
OperationContext tc = OperationContext.allocateGuest();
OperationContext.context.set(tc);
return tc;
}
/**
* @return create a new root context
*/
static public OperationContext allocateRoot() {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContextBuilder().withRootTaskTemplate().toOperationContext();
}
/**
* Sets the current thread to use a new root context
*
* @return create a new root context
*/
static public OperationContext useNewRoot() {
OperationContext tc = OperationContext.allocateRoot();
OperationContext.context.set(tc);
return tc;
}
/*
* Sets the current thread to use a new context
*
* @return create a new context
*/
static public OperationContext use(OperationContextBuilder tcb) {
OperationContext tc = OperationContext.allocate(tcb);
OperationContext.context.set(tc);
return tc;
}
static public OperationContext use(UserContext ctx, OperationContextBuilder tcb) {
OperationContext tc = OperationContext.allocate(ctx, tcb);
OperationContext.context.set(tc);
return tc;
}
/*
* @param m create a task context from a message (RPC calls to dcBus), keep in mind
* this is info gathering only, message must not be allowed to force an
* authenticated/elevated state inappropriately - from RPC clear "Elevated"
* field before calling this
*/
static public OperationContext allocate(Message m) {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContext(m.getFieldAsRecord("Context"));
}
static public OperationContext allocate(RecordStruct ctx) {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContext(ctx);
}
static public OperationContext allocate(UserContext usr, RecordStruct ctx) {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContext(usr, ctx);
}
static public OperationContext allocate(OperationContextBuilder tcb) {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContext(tcb.values);
}
static public OperationContext allocate(UserContext usr, OperationContextBuilder tcb) {
// for occasions where no context is set when calling allocate - we need some context
if (!OperationContext.hasContext())
OperationContext.context.set(OperationContext.defaultcontext);
return new OperationContext(usr, tcb.values);
}
static protected String allocateOpId() {
long num = OperationContext.nextid.getAndIncrement();
// TODO confirm this really does work
/*
if (num > 999999999999999L) {
synchronized (TaskContext.nextid) {
if (TaskContext.nextid.get()> 999999999999999L)
TaskContext.nextid.set(0);
}
num = TaskContext.nextid.getAndIncrement();
}
*/
return OperationContext.getHubId()
+ "_" + OperationContext.getRunId()
+ "_" + StringUtil.leftPad(num + "", 15, '0');
}
/*
* set current thread context to null
*/
static public void clear() {
OperationContext.context.set(null);
}
static public void isGuest(final FuncCallback<Boolean> cb) {
OperationContext.isGuest(OperationContext.get(), cb);
}
/*
* @return check to see if the task is really no more than a guest access. does not change task context
*/
static public void isGuest(OperationContext ctx, final FuncCallback<Boolean> cb) {
if ((ctx == null) || (ctx.userctx == null)) {
cb.setResult(false);
cb.complete();
return;
}
if (ctx.userctx.looksLikeGuest()) {
cb.setResult(true);
cb.complete();
return;
}
ctx.verify(new FuncCallback<UserContext>() {
@Override
public void callback() {
cb.setResult(this.getResult().looksLikeGuest());
cb.complete();
}
});
}
// generally don't mutate a context, but for sign-in support we need to
public static void switchUser(OperationContext ctx, UserContext usr) {
// TODO change only if usr != this.opcontext.getUser
ctx.userctx = usr;
}
// generally don't mutate a context, but sometimes this is fine - use internally only
public static void elevate(OperationContext ctx) {
ctx.opcontext.withField("Elevated", true);
}
// instance code
// ======================================================
// these vars travel with calls to bus
// ======================================================
protected RecordStruct opcontext = null;
protected UserContext userctx = null;
protected DebugLevel level = HubLog.getGlobalLevel();
protected boolean limitLog = true;
protected int logOffset = 0;
protected List<RecordStruct> messages = new ArrayList<>();
// ======================================================
// these vars used only locally, not included in bus calls
// nor in any workqueue calls, these work locally only
// ======================================================
// the current task run, if any
protected WeakReference<TaskRun> taskrun = null;
protected OperationContext parent = null;
protected List<WeakReference<OperationContext>> children = new ArrayList<>();
protected ILocaleResource localeresource = null;
// progress tracking
protected int progTotalSteps = 0;
protected int progCurrStep = 0;
protected String progStepName = null;
protected int progComplete = 0;
protected String progMessage = null;
protected List<IOperationObserver> observers = new CopyOnWriteArrayList<>();
protected IOperationLogger logger = null;
// this tracks time stamp of signs of life from the job writing to the log/progress tracks
// volatile helps keep threads on same page - issue found in code testing and this MAY have helped
volatile protected long lastactivity = System.currentTimeMillis();
// cachable
protected LocaleDefinition localedef = null;
public void touch() {
this.lastactivity = System.currentTimeMillis();
}
// touch parent context too
public void deepTouch() {
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_AMOUNT);
}
public long getLastActivity() {
return this.lastactivity;
}
public void setLimitLog(boolean v) {
this.limitLog = v;
}
public boolean isLimitLog() {
return this.limitLog;
}
public int logMarker() {
return this.logOffset + this.messages.size();
}
// once elevated we can call any service we want, but first we must
// call a service we are allowed to call
/*
* Elevated tasks have been a) authenticated and b) passed successfully into
* a service. Once elevated all subsequent calls with the task no longer need
* to be authenticated or authorized by DivConq framework (individual services/modules
* may require it). Meaning that "guest" cannot call "SendMail" unless it first
* goes through a service that is open to guests, such as password recovery.
*
* Mark the task context as elevated - typically app code does not need to call
* this because the services and scheduler handlers decide when a task has met
* the desired state.
*/
/**
* @return a unique task id - unique across all deployed hub, across runs of a hub
*/
public String getOpId() {
return this.opcontext.getFieldAsString("OpId");
}
/**
* @return the the user context for this task (user context may be shared with other tasks)
*/
public UserContext getUserContext() {
return this.userctx;
}
public DomainInfo getDomain() {
//if (this.schema != null)
// return this.schema;
return this.userctx.getDomain();
}
public SchemaManager getSchema() {
//if (this.schema != null)
// return this.schema;
DomainInfo di = this.userctx.getDomain();
return (di != null) ? di.getSchema() : Hub.instance.getSchema();
}
/**
* not all tasks will have a session, but if there is a session here it is.
*
* id is in the format of hubid_sessionid
*
* @return the id of the session that spawned this task
*
*/
public String getSessionId() {
return this.opcontext.getFieldAsString("SessionId");
}
/**
* not all tasks will have a session, but if there is a session here it is. sessions are local
* to a hub and are not transfered to another hub with the rest of the task info when calling
* a remote service.
*
* @return the session for this task (user context may be shared with other tasks)
*/
public Session getSession() {
return Hub.instance.getSessions().lookup(this.getSessionId());
}
public TaskRun getTaskRun() {
WeakReference<TaskRun> trr = this.taskrun;
if (trr != null)
return trr.get();
return null;
}
public void setTaskRun(TaskRun v) {
this.taskrun = new WeakReference<TaskRun>(v);
}
/**
* @return logging level to use with this task
*/
public DebugLevel getLevel() {
return this.level;
}
public void setLevel(DebugLevel v) {
this.level = v;
}
/**
* Origin indicates where this task originated from. "hub:" means it was started by
* the a hub (task id gives away which hub). "http:[ip address]" means the task
* was started in response to a web request. "ws:[ip address]" means the task
* was started in response to a web scoket request. "ftp:[ip address]" means the task
* was started in response to a ftp request. Etc.
*
* @return origin string
*/
public String getOrigin() {
return this.opcontext.getFieldAsString("Origin");
}
/**
* Elevated tasks have been a) authenticated and b) passed successfully into
* a service. Once elevated all subsequent calls with the task no longer need
* to be authenticated or authorized by DivConq framework (individual services/modules
* may require it). Meaning that "guest" cannot call "SendMail" unless it first
* goes through a service that is open to guests, such as password recovery.
*
* @return true if task has been elevated
*/
public boolean isElevated() {
return this.opcontext.getFieldAsBooleanOrFalse("Elevated");
}
public boolean isGateway() {
return this.opcontext.getFieldAsBooleanOrFalse("Gateway");
}
public void setLocaleResource(ILocaleResource v) {
this.localeresource = v;
}
public ILocaleResource getLocaleResource() {
ILocaleResource tr = this.localeresource;
if (tr != null)
return tr;
tr = this.getDomain();
if (tr != null)
return tr;
return Hub.instance.getResources();
}
// only use during hub booting
protected OperationContext() {
this.opcontext = new RecordStruct();
this.userctx = new UserContext();
}
/**
* @param ctx create a task context from a RecordStruct, keep in mind
* this is info gathering only, call must set
* authenticated/elevated state inappropriately
*/
protected OperationContext(RecordStruct ctx) {
this.opcontext = ctx;
if (ctx.isFieldEmpty("OpId"))
ctx.setField("OpId", OperationContext.allocateOpId());
if (!ctx.isFieldEmpty("DebugLevel"))
this.level = DebugLevel.valueOf(ctx.getFieldAsString("DebugLevel"));
this.userctx = UserContext.allocateFromTask(ctx);
}
protected OperationContext(UserContext usr, RecordStruct ctx) {
this.opcontext = ctx;
if (ctx.isFieldEmpty("OpId"))
ctx.setField("OpId", OperationContext.allocateOpId());
if (!ctx.isFieldEmpty("DebugLevel"))
this.level = DebugLevel.valueOf(ctx.getFieldAsString("DebugLevel"));
this.userctx = usr;
}
public OperationContextBuilder toBuilder() {
return new OperationContextBuilder(this.freezeToRecord());
}
/**
* @param m store task context into a message - for context transfer over bus
*/
public void freeze(Message m) {
m.setField("Context", this.freezeToRecord());
}
public RecordStruct freezeToRecord() {
RecordStruct clone = (RecordStruct) this.opcontext.deepCopy();
this.userctx.freeze(clone);
clone.setField("DebugLevel", this.level.toString());
return clone;
}
public RecordStruct freezeToSafeRecord() {
RecordStruct clone = (RecordStruct) this.opcontext.deepCopy();
this.userctx.freezeSafe(clone);
clone.setField("DebugLevel", this.level.toString());
return clone;
}
// return an approved/verified user context (guest if nothing else)
// verify says - the given auth token, if any, is valid - if there is none then you are a guest and that is valid
//
public void verify(FuncCallback<UserContext> cb) {
if (this.userctx == null) {
cb.errorTr(444);
cb.setResult(UserContext.allocateGuest());
cb.complete();
return;
}
if (this.userctx.isVerified() || this.isElevated()) {
cb.setResult(this.userctx);
cb.complete();
return;
}
System.out.println("doing a verify Op Context");
Message msg = new Message("dcAuth", "Authentication", "Verify");
Hub.instance.getBus().sendMessage(msg, r -> {
if (r.hasErrors())
cb.setResult(UserContext.allocateGuest());
else
cb.setResult(r.getContext().getUserContext());
cb.complete();
});
}
/**
* @param tags to search for with this user
* @return true if this user has one of the requested authorization tags (does not check authentication)
*/
public boolean isAuthorized(String... tags) {
if (this.isElevated())
return true; // always ok
if (!this.userctx.isVerified())
return false;
return this.userctx.isTagged(tags);
}
@Override
public String toString() {
// capture both this and the user
return this.freezeToRecord().toPrettyString();
}
/**
* Overrides any previous return codes and messages
*
* @param code code for message
* @param msg message
*/
public void exit(long code, String msg) {
if (StringUtil.isNotEmpty(msg))
this.log(DebugLevel.Info, code, msg, "Exit");
else
this.boundary("Code", code + "", "Exit");
}
public void clearExitCode() {
this.exit(0, null);
}
// search backward through log to find an error, if we hit a message with an Exit tag then
// stop, as Exit resets Error (unless it is an error itself)
// similar to findExitEntry but stops after last Error as we don't need to loop through all
public boolean hasErrors() {
for (int i = this.messages.size() - 1; i >= 0; i--) {
RecordStruct msg = this.messages.get(i);
if ("Error".equals(msg.getFieldAsString("Level")))
return true;
if (msg.hasField("Tags")) {
ListStruct tags = msg.getFieldAsList("Tags");
if (tags.stringStream().anyMatch(tag -> tag.equals("Exit")))
break;
}
}
return false;
}
public long getCode() {
RecordStruct entry = this.findExitEntry();
if (entry == null)
return 0;
return entry.getFieldAsInteger("Code", 0);
}
public String getMessage() {
RecordStruct entry = this.findExitEntry();
if (entry == null)
return null;
return entry.getFieldAsString("Message");
}
public RecordStruct findExitEntry() {
return this.findExitEntry(0, -1);
}
// search backward through log to find an exit, if we hit a message with an Exit tag then
// stop, as Exit resets Error. now return the first error after Exit. if no errors after
// then return Exit
public RecordStruct findExitEntry(int msgStart, int msgEnd) {
msgStart -= this.logOffset; // adjust so the markers are relative to the current collection of messages, assuming some may have been purged
if (msgEnd == -1)
msgEnd = this.messages.size();
else
msgEnd -= this.logOffset;
RecordStruct firsterror = null;
for (int i = msgEnd - 1; i >= msgStart; i--) {
RecordStruct msg = this.messages.get(i);
if ("Error".equals(msg.getFieldAsString("Level")))
firsterror = msg;
if (msg.hasField("Tags")) {
ListStruct tags = msg.getFieldAsList("Tags");
if (tags.stringStream().anyMatch(tag -> tag.equals("Exit")))
return (firsterror != null) ? firsterror : msg;
}
}
return firsterror;
}
public ListStruct getMessages() {
return new ListStruct(this.messages.toArray());
}
public ListStruct getMessages(int msgStart, int msgEnd) {
msgStart -= this.logOffset; // adjust so the markers are relative to the current collection of messages, assuming some may have been purged
if (msgEnd == -1)
msgEnd = this.messages.size();
else
msgEnd -= this.logOffset;
return new ListStruct(this.messages.subList(msgStart, msgEnd).toArray());
}
/**
* @param code to search for
* @return true if an error code is present
*/
public boolean hasCode(long code) {
return this.hasCode(code, 0, -1);
}
public boolean hasCode(long code, int msgStart, int msgEnd) {
msgStart -= this.logOffset; // adjust so the markers are relative to the current collection of messages, assuming some may have been purged
if (msgEnd == -1)
msgEnd = this.messages.size();
else
msgEnd -= this.logOffset;
for (int i = msgStart; i < msgEnd; i++) {
RecordStruct msg = this.messages.get(i);
if (msg.getFieldAsInteger("Code") == code)
return true;
}
return false;
}
// search backward through log to find an error, if we hit a message with an Exit tag then
// stop, as Exit resets Error (unless it is an error itself)
// similar to findExitEntry but stops after last Error as we don't need to loop through all
public boolean hasLevel(int msgStart, int msgEnd, DebugLevel lvl) {
msgStart -= this.logOffset; // adjust so the markers are relative to the current collection of messages, assuming some may have been purged
if (msgEnd == -1)
msgEnd = this.messages.size();
else
msgEnd -= this.logOffset;
String slvl = lvl.toString();
for (int i = msgStart; i < msgEnd; i++) {
RecordStruct msg = this.messages.get(i);
if (slvl.equals(msg.getFieldAsString("Level")))
return true;
}
return false;
}
public IOperationLogger getLogger() {
return this.logger;
}
public String getLog() {
IOperationLogger logger = this.logger;
if (logger != null)
return logger.logToString();
// TODO reformat these as log entries not as JSON
return this.getMessages().toString();
}
public void error(String message, String... tags) {
this.log(DebugLevel.Error, 1, message, tags);
}
public void error(long code, String message, String... tags) {
this.log(DebugLevel.Error, code, message, tags);
}
public void warn(String message, String... tags) {
this.log(DebugLevel.Warn, 2, message, tags);
}
public void warn(long code, String message, String... tags) {
this.log(DebugLevel.Warn, code, message, tags);
}
public void info(String message, String... tags) {
this.log(DebugLevel.Info, 0, message, tags);
}
public void info(long code, String message, String... tags) {
this.log(DebugLevel.Info, code, message, tags);
}
public void debug(String message, String... tags) {
this.log(DebugLevel.Debug, 0, message, tags);
}
public void debug(long code, String message, String... tags) {
this.log(DebugLevel.Debug, code, message, tags);
}
public void trace(String message, String... tags) {
this.log(DebugLevel.Trace, 0, message, tags);
}
public void trace(long code, String message, String... tags) {
this.log(DebugLevel.Trace, code, message, tags);
}
// let Logger translate to the language of the log file - let tasks translate to their own logs in
// the language of the context
/**
* @param code for message translation token
* @param params for message translation
*/
public void traceTr(long code, Object... params) {
this.logTr(DebugLevel.Trace, code, params);
}
/**
* @param code for message translation token
* @param params for message translation
*/
public void debugTr(long code, Object... params) {
this.logTr(DebugLevel.Debug, code, params);
}
/**
* @param code for message translation token
* @param params for message translation
*/
public void infoTr(long code, Object... params) {
this.logTr(DebugLevel.Info, code, params);
}
/**
* @param code for message translation token
* @param params for message translation
*/
public void warnTr(long code, Object... params) {
this.logTr(DebugLevel.Warn, code, params);
}
/**
* @param code for message translation token
* @param params for message translation
*/
public void errorTr(long code, Object... params) {
this.logTr(DebugLevel.Error, code, params);
}
public void exitTr(long code, Object... params) {
String msg = this.tr("_code_" + code, params);
this.exit(code, msg);
}
/**
* @param lvl level of message
* @param code for message
* @param msg text of message
* @param tags of message
*/
public void log(DebugLevel lvl, long code, String msg, String... tags) {
// must be some sort of message
if (StringUtil.isEmpty(msg))
return;
RecordStruct entry = new RecordStruct(
new FieldStruct("Occurred", new DateTime(DateTimeZone.UTC)),
new FieldStruct("Level", lvl.toString()),
new FieldStruct("Code", code),
new FieldStruct("Message", msg)
);
if (tags.length > 0)
entry.setField("Tags", new ListStruct((Object[])tags));
this.log(entry, lvl);
// pass the message to logger
if (this.getLevel().getCode() >= lvl.getCode()) {
// don't record 0, 1 or 2 - no generic codes
if (code > 2) {
tags = Arrays.copyOf(tags, tags.length + 2);
tags[tags.length - 2] = "Code";
tags[tags.length - 1] = code + "";
}
HubLog.logWr(this.getOpId(), lvl, msg, tags);
}
}
/**
* @param lvl level of message
* @param code for message
* @param params parameters to the message string
*/
public void logTr(DebugLevel lvl, long code, Object... params) {
String msg = this.tr("_code_" + code, params);
RecordStruct entry = new RecordStruct(
new FieldStruct("Occurred", new DateTime(DateTimeZone.UTC)),
new FieldStruct("Level", lvl.toString()),
new FieldStruct("Code", code),
new FieldStruct("Message", msg)
);
this.log(entry, lvl);
// pass the code to logger
if (this.getLevel().getCode() >= lvl.getCode())
HubLog.logWr(this.getOpId(), lvl, code, params);
}
/**
* Add a logging boundary, delineating a new section of work for this task
*
* @param tags identity of this boundary
*/
public void boundary(String... tags) {
RecordStruct entry = new RecordStruct(
new FieldStruct("Occurred", new DateTime(DateTimeZone.UTC)),
new FieldStruct("Level", DebugLevel.Info.toString()),
new FieldStruct("Code", 0),
new FieldStruct("Tags", new ListStruct((Object[])tags))
);
this.log(entry, DebugLevel.Info);
// pass the code to logger
if (this.getLevel().getCode() >= DebugLevel.Info.getCode())
HubLog.boundaryWr(this.getOpId(), tags);
}
// logging is hard on heap and GC - so only do it if necessary
// not generally called by code, internal use mostly
// call this to bypass the Hub logger - for example a bus callback
public void log(RecordStruct entry) {
this.log(entry, DebugLevel.parse(entry.getFieldAsString("Level")));
}
public void log(RecordStruct entry, DebugLevel lvl) {
// think twice about logging debug or trace so we don't overflow the OC log
// always log Info, Error, Warn so it bubbles up and so "hasCode" is relable at those levels
if ((lvl == DebugLevel.Debug) || (lvl == DebugLevel.Trace)) {
if (this.getLevel().getCode() < lvl.getCode())
return;
}
if (this.limitLog) {
while (this.messages.size() > 999) { // no more than 1000 messages when limit is on
this.messages.remove(0);
this.logOffset++;
}
}
// this isn't thread safe, and much of the time it won't be much of an issue
// but could consider Stamp Lock approach to accessing messages array
this.messages.add(entry);
this.fireEvent(OperationEvents.LOG, entry);
}
public void logResult(RecordStruct v) {
ListStruct h = v.getFieldAsList("Messages");
if (h != null) {
for (Struct st : h.getItems())
this.log((RecordStruct) st);
}
}
public boolean isLevel(DebugLevel debug) {
return (this.getLevel().getCode() >= debug.getCode());
}
// progress methods
/**
* @return units/percentage of task completed
*/
public int getAmountCompleted() {
return this.progComplete;
}
/**
* @param v units/percentage of task completed
*/
public void setAmountCompleted(int v) {
this.progComplete = v;
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_AMOUNT);
}
/**
* @return status message about task progress
*/
public String getProgressMessage() {
return this.progMessage;
}
/**
* @param v status message about task progress
*/
public void setProgressMessage(String v) {
this.progMessage = v;
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_MESSAGE);
}
/**
* @param code message translation code
* @param params for the message string
*/
public void setProgressMessageTr(int code, Object... params) {
this.progMessage = this.tr("_code_" + code, params);
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_MESSAGE);
}
/**
* @return total steps for this specific task
*/
public int getSteps() {
return this.progTotalSteps;
}
/**
* @param v total steps for this specific task
*/
public void setSteps(int v) {
this.progTotalSteps = v;
}
/**
* @return current step within this specific task
*/
public int getCurrentStep() {
return this.progCurrStep;
}
/**
* Set step name first, this triggers observers
*
* @param step current step number within this specific task
* @param name current step name within this specific task
*/
public void setCurrentStep(int step, String name) {
this.progCurrStep = step;
this.progStepName = name;
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_STEP);
}
/**
* Set step name first, this triggers observers
*
* @param name current step name within this specific task
*/
public void nextStep(String name) {
this.progCurrStep++;
this.progStepName = name;
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_STEP);
}
/**
* @return name of current step
*/
public String getCurrentStepName() {
return this.progStepName;
}
/**
* @param step number of current step
* @param code message translation code
* @param params for the message string
*/
public void setCurrentStepNameTr(int step, int code, Object... params) {
String name = this.tr("_code_" + code, params);
this.progCurrStep = step;
this.progStepName = name;
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_STEP);
}
/**
* @param code message translation code
* @param params for the message string
*/
public void nextStepTr(int code, Object... params) {
String name = this.tr("_code_" + code, params);
this.progCurrStep++;
this.progStepName = name;
this.fireEvent(OperationEvents.PROGRESS, OperationEvents.PROGRESS_STEP);
}
public void addObserver(IOperationObserver oo) {
// the idea is that we want to unwind the callbacks in LILO order
if (!this.observers.contains(oo))
this.observers.add(0, oo);
if ((oo instanceof IOperationLogger) && (this.logger == null))
this.logger = (IOperationLogger) oo;
}
public int countObservers() {
return this.observers.size();
}
public void removeObserver(IOperationObserver o) {
this.observers.remove(o);
}
public OperationContext subContext() {
OperationContext sub = this.toBuilder().toOperationContext();
sub.setParent(this);
//sub.addObserver(new ParentLogger(this));
this.children.add(new WeakReference<OperationContext>(sub));
return sub;
}
protected void setParent(OperationContext v) {
this.parent = v;
}
// events might fire from external context, keep this in mind
public void fireEvent(OperationEvent event, Object detail) {
this.fireEvent(this, event, detail);
}
// events might fire from external context, keep this in mind
public void fireEvent(OperationContext src, OperationEvent event, Object detail) {
OperationContext curr = OperationContext.get();
try {
this.touch();
for (IOperationObserver ob : this.observers) {
OperationContext.set(this);
ob.fireEvent(event, src, detail);
}
if (this.parent != null) {
if (event == OperationEvents.LOG)
this.parent.log((RecordStruct) detail);
else if (event == OperationEvents.PROGRESS)
this.parent.fireEvent(src, event, detail);
}
// TODO missing concept here
// how do we capture a COMPLETED event after it has passed
// some events need to be flagged as enduring and then kept in a list
// when an observer is added it should be handed the enduring list
// do not use locks here or in addObserver - too expensive - officially
// we only support listeners to COMPLETE, START, PREP, START that
// are added before we start - so we may never want to add any more support
// here, however, if we do, do not include a lock here...that is too expensive
// relative to benefits
}
finally {
OperationContext.set(curr);
}
}
public String getWorkingLocale() {
if (!this.opcontext.isFieldEmpty("OpLocale"))
return this.opcontext.getFieldAsString("OpLocale");
// do not look at user's context, that is a preference only
// op context comes from practical and direct manipulation of the environment we are running in
// how this context started controls the locale
return this.getLocaleResource().getDefaultLocale();
}
// locale definitions must be in domain or hub, not anywhere else
// in a sense this is more accurate than above because it will give the available locale, even if the local setting is set otherwise
public LocaleDefinition getWorkingLocaleDefinition() {
if (this.localedef == null) {
String locale = this.getWorkingLocale();
ILocaleResource tr = this.getLocaleResource();
this.localedef = tr.getLocaleDefinition(locale);
}
return this.localedef;
}
// 0 is best, higher the number the worse, -1 for not supported
@Override
public int rateLocale(String locale) {
LocaleDefinition def = this.getWorkingLocaleDefinition();
if (def.match(locale))
return 0;
int rate = this.getLocaleResource().rateLocale(locale);
if (rate < 0)
return -1;
return rate + 1;
}
@Override
public LocaleDefinition getLocaleDefinition(String locale) {
ILocaleResource tr = this.getLocaleResource();
return tr.getLocaleDefinition(locale);
}
public String tr(String token, Object... params) {
ILocaleResource tr = this.getLocaleResource();
LocaleDefinition def = this.getWorkingLocaleDefinition();
return tr.getDictionary().tr(tr, def, token, params);
}
public String trp(String pluraltoken, String singulartoken, Object... params) {
ILocaleResource tr = this.getLocaleResource();
LocaleDefinition def = this.getWorkingLocaleDefinition();
return tr.getDictionary().trp(tr, def, pluraltoken, singulartoken, params);
}
public String findToken(String token) {
ILocaleResource tr = this.getLocaleResource();
LocaleDefinition def = this.getWorkingLocaleDefinition();
return tr.getDictionary().findToken(tr, def, token);
}
}