//----------------------------------------------------------------------------
// Copyright (C) 2011 Ingrid Nunes
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// To contact the authors:
// http://inf.ufrgs.br/prosoft/bdi4jade/
//
//----------------------------------------------------------------------------
package bdi4jade.core;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import bdi4jade.annotation.GoalOwner;
import bdi4jade.event.GoalListener;
import bdi4jade.exception.PlanInstantiationException;
import bdi4jade.goal.Goal;
import bdi4jade.goal.GoalStatus;
import bdi4jade.plan.Plan;
import bdi4jade.plan.Plan.EndState;
import bdi4jade.plan.planbody.PlanBody;
/**
* This class represents the intention abstraction from the BDI model. It
* represents a goal that the agent is committed to achieve. It has the
* associated goal and tries to execute plans to achieve it. It keeps a list of
* the executed plans, and after using all plans unsuccessfully, the goal is
* considered unachievable. When a plan fails, the BDI-interpreter cycle may
* invoke the {@link #tryToAchive()} method again, so the intention tries
* another plan. During its execution, the intention can be set to no longer
* desired. This occurs during the agent reasoning cycle or when a goal is
* dropped ({@link BDIAgent#dropGoal(Goal)}).
*
* @author Ingrid Nunes
*/
public class Intention {
private PlanBody currentPlan;
private final Capability dispatcher;
private final Set<Plan> executedPlans;
private final Goal goal;
private final List<GoalListener> goalListeners;
private final Log log;
private final AbstractBDIAgent myAgent;
private boolean noLongerDesired;
private final Set<Capability> owners;
private boolean unachievable;
private boolean waiting;
/**
* Creates a new intention. It is associated with an agent and the goal that
* it is committed to achieve.
*
* @param goal
* the goal to be achieved.
* @param bdiAgent
* the bdiAgent associated with this intention.
*/
public Intention(Goal goal, AbstractBDIAgent bdiAgent)
throws IllegalAccessException {
this(goal, bdiAgent, null);
}
/**
* Creates a new intention. It is associated with an agent and the goal that
* it is committed to achieve. It also receives a {@link Capability} as
* parameter indicating the owner of the goal (dispatched the goal).
*
* @param goal
* the goal to be achieved.
* @param bdiAgent
* the bdiAgent associated with this intention.
* @param dispatcher
* the Capability that dispatched the goal.
*/
public Intention(Goal goal, AbstractBDIAgent bdiAgent, Capability dispatcher)
throws IllegalAccessException {
this.log = LogFactory.getLog(this.getClass());
this.goal = goal;
this.myAgent = bdiAgent;
this.unachievable = false;
this.noLongerDesired = false;
this.waiting = true;
this.executedPlans = new HashSet<>();
this.currentPlan = null;
this.goalListeners = new LinkedList<>();
this.dispatcher = dispatcher;
Class<? extends Capability> owner = null;
boolean internal = false;
if (goal.getClass().isAnnotationPresent(GoalOwner.class)) {
GoalOwner ownerAnnotation = goal.getClass().getAnnotation(
GoalOwner.class);
owner = ownerAnnotation.capability();
internal = ownerAnnotation.internal();
} else {
Class<?> enclosingClass = goal.getClass().getEnclosingClass();
if (enclosingClass != null
&& Capability.class.isAssignableFrom(goal.getClass()
.getEnclosingClass())) {
owner = (Class<Capability>) enclosingClass;
}
}
if (owner == null) {
this.owners = new HashSet<>();
} else {
if (dispatcher == null) {
this.owners = myAgent.getGoalOwner(owner, internal);
} else {
this.owners = dispatcher.getGoalOwner(owner, internal);
if (owners.isEmpty()) {
throw new IllegalAccessException("Capability " + dispatcher
+ " has no access to goal "
+ goal.getClass().getName() + " of capability "
+ owner.getName());
}
}
}
}
/**
* Adds a listener to be notified when about goal events.
*
* @param goalListener
* the listener to be notified.
*/
public void addGoalListener(GoalListener goalListener) {
synchronized (goalListeners) {
goalListeners.add(goalListener);
}
}
/**
* Dispatches a new plan to try to achieve the intention goal. It looks for
* plans that can achieve the goal that were not already tried and then
* starts the plan. If all possible plans were already executed, the
* intention is set to unachievable.
*/
private synchronized void dispatchPlan() {
Map<Capability, Set<Plan>> options = new HashMap<>();
if (owners.isEmpty()) {
for (Capability capability : myAgent.getCapabilities()) {
capability.addCandidatePlans(goal, options);
}
} else {
for (Capability capability : owners) {
capability.addCandidatePlans(goal, options);
}
}
Iterator<Capability> it = options.keySet().iterator();
while (it.hasNext()) {
Set<Plan> plans = options.get(it.next());
plans.removeAll(executedPlans);
if (plans.isEmpty()) {
it.remove();
}
}
while (this.currentPlan == null && !options.isEmpty()) {
Plan selectedPlan = myAgent.getPlanSelectionStrategy().selectPlan(
goal, options);
try {
this.currentPlan = selectedPlan.createPlanBody();
currentPlan.init(selectedPlan, this);
} catch (PlanInstantiationException e) {
log.error("Plan " + selectedPlan.getId()
+ " could not be instantiated.");
e.printStackTrace();
this.currentPlan = null;
for (Set<Plan> plans : options.values()) {
plans.remove(selectedPlan);
}
}
}
if (options.isEmpty()) {
this.unachievable = true;
} else {
this.currentPlan.start();
}
}
/**
* Sets this intention to the {@link GoalStatus#WAITING} status. It may come
* from the {@link GoalStatus#PLAN_FAILED} or
* {@link GoalStatus#TRYING_TO_ACHIEVE} states.
*/
public synchronized void doWait() {
GoalStatus status = getStatus();
switch (status) {
case WAITING:
break;
case TRYING_TO_ACHIEVE:
this.waiting = true;
this.currentPlan.stop();
this.currentPlan = null;
break;
case PLAN_FAILED:
this.waiting = true;
this.executedPlans.add(this.currentPlan.getPlan());
this.currentPlan = null;
break;
default:
assert false : status;
break;
}
}
/**
* Returns the capability that dispatched this goal.
*
* @return the dispatcher.
*/
public Capability getDispatcher() {
return dispatcher;
}
/**
* Returns the goal associated with this intention.
*
* @return the goal.
*/
public Goal getGoal() {
return goal;
}
/**
* Returns all goal listeners.
*
* @return the goalListeners.
*/
public List<GoalListener> getGoalListeners() {
return goalListeners;
}
/**
* Returns the agent associated with this intention.
*
* @return the myAgent.
*/
public AbstractBDIAgent getMyAgent() {
return myAgent;
}
/**
* Returns the set of capabilities that own this goal.
*
* @return the owners.
*/
public Set<Capability> getOwners() {
return owners;
}
/**
* Returns the current goal status that this capability is committed to
* achieve.
*
* @return the current goal status.
*
* @see GoalStatus
*/
public synchronized GoalStatus getStatus() {
if (this.unachievable) {
return GoalStatus.UNACHIEVABLE;
} else if (this.noLongerDesired) {
return GoalStatus.NO_LONGER_DESIRED;
} else if (this.waiting) {
return GoalStatus.WAITING;
} else if (this.currentPlan == null) {
return GoalStatus.TRYING_TO_ACHIEVE;
} else {
EndState endState = this.currentPlan.getEndState();
if (EndState.FAILED.equals(endState)) {
return GoalStatus.PLAN_FAILED;
} else if (EndState.SUCCESSFUL.equals(endState)) {
return GoalStatus.ACHIEVED;
} else {
return GoalStatus.TRYING_TO_ACHIEVE;
}
}
}
/**
* Sets this intention as no longer desired. It stops the current plan
* execution. It changes the goal status from {@link GoalStatus#WAITING},
* {@link GoalStatus#PLAN_FAILED} or {@link GoalStatus#TRYING_TO_ACHIEVE} to
* {@link GoalStatus#NO_LONGER_DESIRED}.
*/
public synchronized void noLongerDesire() {
GoalStatus status = getStatus();
switch (status) {
case WAITING:
this.noLongerDesired = true;
break;
case TRYING_TO_ACHIEVE:
this.noLongerDesired = true;
this.currentPlan.stop();
this.currentPlan = null;
break;
case PLAN_FAILED:
this.noLongerDesired = true;
this.executedPlans.add(this.currentPlan.getPlan());
this.currentPlan = null;
break;
default:
assert false : status;
break;
}
}
/**
* Removes a goal listener, so it will not be notified about the goal events
* anymore.
*
* @param goalListener
* the goal listener to be removed.
*/
public void removeGoalListener(GoalListener goalListener) {
synchronized (goalListeners) {
goalListeners.remove(goalListener);
}
}
/**
* Makes this intention starts to try to achieve the goal. It changes the
* goal status from {@link GoalStatus#WAITING} or
* {@link GoalStatus#PLAN_FAILED} to {@link GoalStatus#TRYING_TO_ACHIEVE}.
*/
public synchronized void tryToAchive() {
GoalStatus status = getStatus();
switch (status) {
case TRYING_TO_ACHIEVE:
break;
case WAITING:
this.waiting = false;
dispatchPlan();
break;
case PLAN_FAILED:
this.executedPlans.add(this.currentPlan.getPlan());
this.currentPlan = null;
dispatchPlan();
break;
default:
assert false : status;
break;
}
}
}