package org.techniques.scorm.sn;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import org.techniques.scorm.cam.CourseItem;
import org.techniques.scorm.cam.Organization;
import org.techniques.scorm.cmi.SequencingImpactListener;
import org.techniques.util.Externalizer;
/**
* Represents a hierarchical structure of Learning Activities.
*
* @author Timothy Potter
* @version #kworks-1.3#
*/
public class ActivityTree extends Cluster implements SequencingImpactListener, Comparable, Externalizable {
static final long serialVersionUID = 2249414318158898793L;
public static String toNavRequestString(int code) {
switch (code) {
case START:
return "START";
case RESUME_ALL:
return "RESUME_ALL";
case CONTINUE:
return "CONTINUE";
case PREVIOUS:
return "PREVIOUS";
case FORWARD:
return "FORWARD";
case BACKWARD:
return "BACKWARD";
case CHOICE:
return "CHOICE";
default:
return "INVALID";
}
}
protected String learnerId = null;
protected String courseId = null;
protected String currentActivity = null;
protected String suspendedActivity = null;
protected boolean objectivesGlobalToSystem = true;
protected HashMap globalObjectiveMap = null;
private transient Organization t_org = null; // cannot process requests until set
private transient ArrayList t_actSeq = null;
private transient HashMap t_actMap = null;
private transient Activity t_current = null;
private transient Activity t_suspended = null;
private transient FlowRequest t_flowRequest = new FlowRequest();
/**
* Only use for de-serialization via the readExternal method.
* @see readExternal(java.io.ObjectInput)
*/
public ActivityTree() { super(); }
public ActivityTree(String id, String learnerId, String courseId) {
super(Activity.TREE, id);
this.learnerId = learnerId;
this.courseId = courseId;
}
public String getLearnerId() { return learnerId; }
public String getCourseId() { return courseId; }
public boolean isComplete() {
boolean isComplete = false;
if (progressStatus) {
if (attempts != null && attempts.length > 0) {
Attempt lastAttempt = attempts[attempts.length-1];
if (lastAttempt.progressStatus) {
if (lastAttempt.completionStatus) {
isComplete = true;
}
}
}
}
if (isComplete) {
RuleCondition isSat = new RuleCondition();
isSat.condition = RuleCondition.SATISFIED;
isSat.operator = RuleCondition.NO_OP;
isComplete = (isSat.evaluate(this) == TRUE);
}
return isComplete;
}
public boolean getObjectivesGlobalToSystem() {
return this.objectivesGlobalToSystem;
}
public void setCurrentActivity(String id) {
this.currentActivity = id;
this.t_current = null;
}
public void setSuspendedActivity(String id) {
this.suspendedActivity = id;
this.t_suspended = null;
}
public Activity getCurrentActivity() {
if (t_current == null) {
t_current = (currentActivity != null) ? getActivity(currentActivity) : null;
}
return t_current;
}
public Activity getSuspendedActivity() {
if (t_suspended == null) {
t_suspended = (suspendedActivity != null) ? getActivity(suspendedActivity) : null;
}
return t_suspended;
}
public int getStartNavigationRequest() {
if (currentActivity == null) {
return (suspendedActivity != null) ? RESUME_ALL : START;
} else {
Activity current = getCurrentActivity();
if (current.isActive) {
processTerminationRequest(EXIT);
setCurrentActivity(null);
return START;
} else {
return current.isSuspended ? RESUME_ALL : INVALID_REQUEST;
}
}
}
public void setRuntimeDependencies(Organization org, HashMap globalObjectiveMap) {
t_org = org;
// todo: this is shite! current design is if objectives are global to system
// then they are persisted via the RTE, else they are persisted internally
// with the activity tree
if (t_org.getObjectivesGlobalToSystem()) {
this.objectivesGlobalToSystem = true;
this.globalObjectiveMap = globalObjectiveMap;
} else {
this.objectivesGlobalToSystem = false;
if (this.globalObjectiveMap == null) {
// not read from stream...setup
if (t_org.hasGlobalObjectives()) {
this.globalObjectiveMap = new HashMap();
Iterator it = t_org.getGlobalObjectives();
while (it.hasNext()) {
String globalObjId = (String)it.next();
ObjectiveProgress op = new ObjectiveProgress();
op.id = globalObjId;
this.globalObjectiveMap.put(globalObjId, op);
}
}
}
}
if (t_actSeq != null) {
t_actSeq.clear();
} else {
t_actSeq = new ArrayList();
}
appendActivityToSequence(this);
if (t_actMap != null) {
t_actMap.clear();
} else {
t_actMap = new HashMap();
}
Iterator i = t_actSeq.iterator();
int p = -1;
while (i.hasNext()) {
++p;
Activity act = (Activity)i.next();
if (act != null) {
String actId = act.getId();
CourseItem actItem = t_org.getItem(actId);
if (actItem != null) {
Sequencing seq = actItem.getSequencing();
if (seq == null) seq = Sequencing.DEFAULT;
act.setRuntimeDependencies(this, seq);
t_actMap.put(actId, act);
} else {
System.out.println("WARNING!!! No CourseItem found for activity ["+actId+"]");
}
} else {
System.out.println("WARNING!!! t_actSeq had a null activity around position ["+p+"]");
}
}
}
public Iterator getActivityList() {
return (t_actSeq != null) ? t_actSeq.iterator() : null;
}
public Iterator getGlobalObjectives() {
return (globalObjectiveMap != null) ? globalObjectiveMap.values().iterator() : null;
}
public ObjectiveProgress getGlobalObjectiveProgress(String objId) {
ObjectiveProgress objPrg = null;
if (objId != null && globalObjectiveMap != null) {
objPrg = (ObjectiveProgress)globalObjectiveMap.get(objId);
if (objPrg == null) {
objPrg = new ObjectiveProgress();
objPrg.id = objId;
globalObjectiveMap.put(objId, objPrg);
}
}
return objPrg;
}
// [OP.1] Overall Sequencing Process
// [NB.2.1] Navigation Request Process
public Activity processNavRequest(int navreqCode) {
switch (navreqCode) {
case START:
return processNavStartRequest();
case RESUME_ALL:
return processNavResumeAllRequest();
case CONTINUE:
return processNavContinueRequest();
case PREVIOUS:
return processNavPreviousRequest();
case FORWARD:
case BACKWARD:
throw new SequencingException("NB.2.1-7");
default:
throw new SequencingException("NB.2.1-1");
}
}
// [NB.2.1] Navigation Request Process - Start
public Activity processNavStartRequest() {
if (currentActivity == null) {
return processSeqStartRequest();
} else {
throw new SequencingException("NB.2.1-1");
}
}
// [NB.2.1] Navigation Request Process - Resume All
public Activity processNavResumeAllRequest() {
if (currentActivity == null) {
if (suspendedActivity != null) {
return processSeqResumeAllRequest();
} else {
throw new SequencingException("NB.2.1-3");
}
} else {
throw new SequencingException("NB.2.1-1");
}
}
// [NB.2.1] Navigation Request Process - Suspend All
public void processNavSuspendAllRequest() {
if (currentActivity != null) {
processTerminationRequest(SUSPEND_ALL);
processSeqExitRequest();
setCurrentActivity(null); // todo: find this in the pseudo-code, resume all doesn't work otherwise
} else {
throw new SequencingException("NB.2.1-2");
}
}
public void processNavExitRequest() {
if (currentActivity != null) {
Activity current = getCurrentActivity();
if (current.isActive) {
processTerminationRequest(EXIT);
processSeqExitRequest();
} else {
throw new SequencingException("NB.2.1-12");
}
} else {
throw new SequencingException("NB.2.1-2");
}
}
public void processNavExitAllRequest() {
if (currentActivity != null) {
processTerminationRequest(EXIT_ALL);
processSeqExitRequest();
setCurrentActivity(null);
setSuspendedActivity(null);
} else {
throw new SequencingException("NB.2.1-2");
}
}
public void processNavAbandonRequest() {
if (currentActivity != null) {
Activity current = getCurrentActivity();
if (current.isActive) {
processTerminationRequest(ABANDON);
processSeqExitRequest();
} else {
throw new SequencingException("NB.2.1-12");
}
} else {
throw new SequencingException("NB.2.1-2");
}
}
public void processNavAbandonAllRequest() {
if (currentActivity != null) {
processTerminationRequest(ABANDON_ALL);
processSeqExitRequest();
setCurrentActivity(null);
} else {
throw new SequencingException("NB.2.1-2");
}
}
// [NB.2.1] Navigation Request Process - Continue
public Activity processNavContinueRequest() {
if (currentActivity == null) {
Activity suspendedActivity = getSuspendedActivity();
if(suspendedActivity == null) {
throw new SequencingException("NB.2.1-2");
} else {
return processSeqResumeAllRequest();
}
}
Activity act = getCurrentActivity();
if (this.equals(act)) {
throw new SequencingException("NB.2.1-4");
}
Activity parent = act.getParent();
if (!parent.seq.controlMode.flow) {
SequencingException seqExc = new SequencingException("NB.2.1-4");
if (parent.seq.controlMode.choice) {
seqExc.showChoice = parent;
}
throw seqExc;
}
if (act.isActive) {
Activity fromTerm = processSeqRequest(processTerminationRequest(EXIT));
if (fromTerm != null) return fromTerm;
}
return processSeqContinueRequest();
}
// [NB.2.1] Navigation Request Process - Previous
public Activity processNavPreviousRequest() {
if (currentActivity == null) {
Activity suspendedActivity = getSuspendedActivity();
if(suspendedActivity == null) {
throw new SequencingException("NB.2.1-2");
} else {
return processSeqResumeAllRequest();
}
}
Activity act = getCurrentActivity();
if (this.equals(act)) throw new SequencingException("NB.2.1-6");
Activity parent = act.getParent();
if (!parent.seq.controlMode.flow) throw new SequencingException("NB.2.1-5");
if (parent.seq.controlMode.forwardOnly) throw new SequencingException("NB.2.1-5");
if (act.isActive) {
Activity fromTerm = processSeqRequest(processTerminationRequest(EXIT));
if (fromTerm != null) return fromTerm;
}
return processSeqPreviousRequest();
}
// [NB.2.1] Navigation Request Process - Choice
public Activity processNavChoiceRequest(String choice) {
Activity target = getActivity(choice);
if (target == null) throw new SequencingException("NB.2.1-11");
Activity parent = target.getParent();
if (parent != null) {
if (!parent.seq.controlMode.choice) throw new SequencingException("NB.2.1-10");
}
if (currentActivity == null) {
return processSeqChoiceRequest(target);
} else {
Activity current = getCurrentActivity();
Activity currentParent = current.getParent();
if (currentParent != null) {
if (!currentParent.equals(parent)) {
// target is not a sibling of current activity
Activity commonAncestor = target.findCommonAncestor(current);
Activity[] pathToAncestor = current.getPathToAncestor(commonAncestor);
if (pathToAncestor == null || pathToAncestor.length == 0) throw new SequencingException("NB.2.1-9");
int excludeAncestor = pathToAncestor.length-1; // exclude last element in array
for (int i=0; i currentIndex) {
direction = FORWARD_TRAVERSAL;
int diff = targetIndex-currentIndex;
pathFromCurrent = new Activity[diff];
int bounds = currentIndex+diff;
for (int i=currentIndex; i targetIndex; i--) {
pathFromCurrent[currentIndex-i] = (Activity)targetParent.childList.get(i);
}
}
if (pathFromCurrent == null || pathFromCurrent.length == 0) throw new SequencingException("SB.2.9-5");
// pathFromCurrent already excludes the target
for (int i=0; i constrIndex) ? FORWARD_TRAVERSAL : BACKWARD_TRAVERSAL;
Activity toConsider = constrained.applyChoiceFlow(direction);
Activity[] pathToRoot = target.getPathToRoot();
boolean isDescendentOf = false;
for (int i=0; i current.getIndex()) ? FORWARD_TRAVERSAL : BACKWARD_TRAVERSAL;
if (direction == FORWARD_TRAVERSAL) {
for (int i=0; i<(pathFromAncestor.length-1); i++) { // exclusive of target
pathFromAncestor[i].applyChoiceActivityTraversal(FORWARD_TRAVERSAL);
if (!pathFromAncestor[i].isActive && (!pathFromAncestor[i].equals(commonAncestor) && pathFromAncestor[i].seq.constrainedChoiceConsiderations.preventActivation)) {
throw new SequencingException("SB.2.9-6");
}
}
} else {
for (int i=0; i<(pathFromAncestor.length-1); i++) { // exclusive of target
if (!pathFromAncestor[i].isActive && (!pathFromAncestor[i].equals(commonAncestor) && pathFromAncestor[i].seq.constrainedChoiceConsiderations.preventActivation)) {
throw new SequencingException("SB.2.9-6");
}
}
}
}
} else {
throw new SequencingException("SB.2.9-5"); // nothing to deliver
}
}
}
}
}
if (target.isLeaf()) return processDeliveryRequest(target);
t_flowRequest.reset();
t_flowRequest.currentDirection = FORWARD_TRAVERSAL;
t_flowRequest.previousDirection = UNDEFINED_TRAVERSAL;
t_flowRequest.considerChildren = true;
try {
target.applyFlow(t_flowRequest);
return processDeliveryRequest(t_flowRequest.activity);
} catch (SequencingException seqExc) {
//seqExc.printStackTrace();
if (commonAncestor != null) {
commonAncestor.applyTerminateDescendentAttemptsProcess(current, EXIT);
commonAncestor.endAttempt();
setCurrentActivity(target.id);
}
if (target.seq.controlMode.choice) {
SequencingException se = new SequencingException("SB.2.9-9");
se.showChoice = target; // allows the sequencing engine to recover
throw se;
} else {
throw new SequencingException("SB.2.9-9");
}
}
}
// [SB.2.10] Retry Sequencing Request Process
Activity processSeqRetryRequest() {
if (currentActivity == null) throw new SequencingException("SB.2.10-1");
Activity current = getCurrentActivity();
if (current.isActive || current.isSuspended) throw new SequencingException("SB.2.10-2");
if (current.isLeaf()) {
return processDeliveryRequest(current);
} else {
t_flowRequest.reset();
t_flowRequest.currentDirection = FORWARD_TRAVERSAL;
t_flowRequest.previousDirection = UNDEFINED_TRAVERSAL;
t_flowRequest.considerChildren = true;
current.applyFlow(t_flowRequest);
return processDeliveryRequest(t_flowRequest.activity);
}
}
// [SB.2.11] Exit Sequencing Request Process
boolean processSeqExitRequest() {
if (currentActivity == null) throw new SequencingException("SB.2.11-1");
Activity current = getCurrentActivity();
if (current.isActive) throw new SequencingException("SB.2.11-2");
if (this.equals(current)) {
return true;
} else {
return false;
}
}
// [DB.1.1] and [DB.2] Content Delivery Environment Process
Activity processDeliveryRequest(Activity target) {
if (!target.isLeaf()) {
if (target.seq.controlMode.choice) {
SequencingException se = new SequencingException("DB.1.1-1");
se.showChoice = target; // allows the sequencing engine to recover
throw se;
} else {
throw new SequencingException("DB.1.1-1");
}
}
Activity[] pathFromRoot = target.getPathFromRoot();
if (pathFromRoot == null || pathFromRoot.length == 0) throw new SequencingException("DB.1.1-2");
for (int i=0; i 0) {
for (int i=0; i 0) {
for (int i=0; i= obj.minNormalizedMeasure) {
act.objs[i].satisfiedStatus = true;
} else {
act.objs[i].satisfiedStatus = false;
}
}
if (obj.maps != null) {
for (int m=0; m= obj.minNormalizedMeasure) {
globalObj.satisfiedStatus = true;
if (globalObj.satisfiedOn == ObjectiveProgress.NOT_SATISFIED_YET) globalObj.satisfiedOn = System.currentTimeMillis();
} else {
globalObj.satisfiedStatus = false;
globalObj.satisfiedOn = ObjectiveProgress.NOT_SATISFIED_YET;
}
}
}
}
}
}
break;
}
}
}
}
// cmi.exit affects navigation requests
public void onExitUpdate(String scoId, String exit) {
if ("suspend".equals(exit)) {
if (currentActivity != null) {
Activity current = getCurrentActivity();
current.isSuspended = true;
current.isActive = false;
try {
// this is an attempt to rollup the current activity's state
// after Terminate so that the tree is up-to-date
current.rollupCurrentAttemptState();
} catch (SequencingException seqExc) {
seqExc.printStackTrace();
}
setSuspendedActivity(currentActivity);
setCurrentActivity(null);
} else {
processNavSuspendAllRequest();
}
} else if ("time-out".equals(exit)) {
processNavExitAllRequest();
} else if ("logout".equals(exit)) {
processNavSuspendAllRequest();
}
}
public String toString() {
StringBuffer sb = new StringBuffer(super.toString());
sb.append("; learnerId=").append(String.valueOf(learnerId));
sb.append("; courseId=").append(String.valueOf(courseId));
sb.append("; currentActivity=").append(String.valueOf(currentActivity));
sb.append("; suspendedActivity=").append(String.valueOf(suspendedActivity));
return sb.toString();
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
super.readExternal(in);
learnerId = Externalizer.safeReadUTF(in);
courseId = Externalizer.safeReadUTF(in);
setCurrentActivity(Externalizer.safeReadUTF(in));
setSuspendedActivity(Externalizer.safeReadUTF(in));
objectivesGlobalToSystem = in.readBoolean();
if (in.readBoolean()) {
globalObjectiveMap = (HashMap)in.readObject();
}
}
public void writeExternal(ObjectOutput out) throws IOException {
super.writeExternal(out);
Externalizer.safeWriteUTF(out, learnerId);
Externalizer.safeWriteUTF(out, courseId);
Externalizer.safeWriteUTF(out, currentActivity);
Externalizer.safeWriteUTF(out, suspendedActivity);
out.writeBoolean(objectivesGlobalToSystem);
if (objectivesGlobalToSystem) {
out.writeBoolean(false);
} else {
if (globalObjectiveMap != null) {
out.writeBoolean(true);
out.writeObject(globalObjectiveMap);
} else {
out.writeBoolean(false);
}
}
}
}