/* * AbstractSkill.java * * Created on Jan 16, 2008, 10:57:22 AM * * Description: A skill that provides session-awareness for another skill. * * Copyright (C) Jan 16, 2008 Stephen L. Reed. * * This program is free software; you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.texai.ahcsSupport; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.TimerTask; import java.util.UUID; import net.jcip.annotations.ThreadSafe; import org.apache.log4j.Logger; import org.joda.time.Duration; import org.openrdf.model.URI; import org.texai.ahcsSupport.AHCSConstants.State; import org.texai.util.StringUtils; import org.texai.util.TexaiException; /** A skill that provides session-awareness for another skill. * * @author Stephen L. Reed */ @ThreadSafe public class SessionManagerSkill extends AbstractSkill { /** the logger */ private static final Logger LOGGER = Logger.getLogger(SessionManagerSkill.class); /** the class of the managed sub-skill */ private Class<?> skillClass; /** the session dictionary, session handle --> managed skill instance */ private final Map<String, SkillInfo> sessionDictionary = new HashMap<>(); /** the duration beyond which unused skill instances are garbage collected */ private long cleanupDurationTimeMillis = 1000 * 60 * 60 * 24; // defaults to one day /** the dummy session handle */ private static final String DUMMY_SESSION_HANDLE = "dummy session handle"; /** Constructs a new Skill instance. */ public SessionManagerSkill() { } /** Returns the id of the containing role. * * @return the id of the containing role */ @Override public URI getRoleId() { return getRole().getId(); } /** Returns the class name of this skill. * * @return the class name of this skill */ @Override public String getClassName() { return getClass().getName(); } /** Receives and attempts to process the given message. The skill is thread safe, given that any contained libraries are single threaded * with regard to the conversation. * * @param message the given message * @return whether the message was successfully processed */ @Override public boolean receiveMessage(final Message message) { //Preconditions assert message != null : "message must not be null"; final String operation = message.getOperation(); switch (operation) { case AHCSConstants.AHCS_INITIALIZE_TASK: assert getSkillState().equals(State.UNINITIALIZED) : "prior state must be non-initialized"; getNodeRuntime().getTimer().scheduleAtFixedRate( new SkillInfoJanitorProcess(), // task 3_600_000, // delay - one hour 3_600_000); // period - one hour // initialize a dummy instance of the role getSkillInstance(DUMMY_SESSION_HANDLE).receiveMessage(message); // initialize child roles propagateOperationToChildRoles(operation); setSkillState(State.INITIALIZED); return true; case AHCSConstants.AHCS_READY_TASK: assert getSkillState().equals(State.INITIALIZED) : "prior state must be initialized"; // ready a dummy instance of the role getSkillInstance(DUMMY_SESSION_HANDLE).receiveMessage(message); // ready child roles propagateOperationToChildRoles(operation); setSkillState(State.READY); return true; } assert operation.equals(AHCSConstants.REGISTER_SENSED_UTTERANCE_PROCESSOR_TASK) || getSkillState().equals(State.READY) : "must be in the ready state, but is " + stateDescription(getSkillState()) + "\nmessage: " + message.toString(getNodeRuntime()); if (LOGGER.isDebugEnabled()) { LOGGER.info("delegating " + message.getOperation() + " to " + getSkillInstance(getSessionHandle(message))); } return getSkillInstance(getSessionHandle(message)).receiveMessage(message); } /** Synchronously processes the given message. The skill is thread safe, given that any contained libraries are single threaded * with regard to the conversation. * * @param message the given message * @return the response message or null if not applicable */ @Override public Message converseMessage(final Message message) { //Preconditions assert message != null : "message must not be null"; if (LOGGER.isDebugEnabled()) { LOGGER.info("delegating " + message.getOperation() + " to " + getSkillInstance(getSessionHandle(message))); } return getSkillInstance(getSessionHandle(message)).converseMessage(message); } /** Returns the skill instance that corresponds to the session handle. * * @param sessionHandle the session handle * @return the skill instance that corresponds to the session handle */ private AbstractSkill getSkillInstance(final String sessionHandle) { //Preconditions assert sessionHandle != null : "sessionHandle must not be null"; assert !sessionHandle.isEmpty() : "sessionHandle must not be empty"; assert skillClass != null : "skillClass must not be null"; final AbstractSkill skill; synchronized (sessionDictionary) { SkillInfo skillInfo = sessionDictionary.get(sessionHandle); if (skillInfo == null) { try { skill = (AbstractSkill) skillClass.newInstance(); } catch (InstantiationException | IllegalAccessException ex) { throw new TexaiException(ex); } skill.setRole(getRole()); skill.setSessionManagerSkill(this); skillInfo = new SkillInfo(skill, sessionHandle); sessionDictionary.put(sessionHandle, skillInfo); if (!sessionHandle.equals(DUMMY_SESSION_HANDLE)) { // initialize the skill instance Message message = new Message( getRoleId(), // senderRoleId getClassName(), // senderService getRoleId(), // recipientRoleId skillClass.getName(), // service AHCSConstants.AHCS_INITIALIZE_TASK); // operation skill.receiveMessage(message); // ready the skill instance message = new Message( getRoleId(), // senderRoleId getClassName(), // senderService getRoleId(), // recipientRoleId skillClass.getName(), // service AHCSConstants.AHCS_READY_TASK); // operation skill.receiveMessage(message); } } else { skill = skillInfo.skill; } } //Postconditions assert skill != null : "skill must not be null"; return skill; } /** Disconnects the session. * * @param sessionHandle the session handle */ public void disconnectSession(final String sessionHandle) { //Preconditions assert StringUtils.isNonEmptyString(sessionHandle) : "sessionHandle must be a non-empty string"; synchronized (sessionDictionary) { final SkillInfo skillInfo = sessionDictionary.remove(sessionHandle); if (skillInfo != null) { LOGGER.info("removed " + skillInfo.skill.getClass().getSimpleName() + " because session disconnected " + sessionHandle); } } } /** Gets the session handle from the given message. * * @param message the given message * @return the session handle */ public String getSessionHandle(final Message message) { //Preconditions assert message != null : "message must not be null"; String sessionHandle = (String) message.get(AHCSConstants.SESSION); if (sessionHandle == null) { sessionHandle = UUID.randomUUID().toString(); } return sessionHandle; } /** Sets the class of the managed sub-skill. * * @param skillClass the class of the managed sub-skill */ public void setSkillClass(Class<?> skillClass) { this.skillClass = skillClass; } /** Gets the duration beyond which unused skill instances are garbage collected. * * @return the duration beyond which unused skill instances are garbage collected */ public long getCleanupDurationTimeMillis() { return cleanupDurationTimeMillis; } /** Sets the duration beyond which unused skill instances are garbage collected. * * @param cleanupDurationTimeMillis the duration beyond which unused skill instances are garbage collected */ public void setCleanupDurationTimeMillis(long cleanupDurationTimeMillis) { this.cleanupDurationTimeMillis = cleanupDurationTimeMillis; } /** Provides a periodic task that iterates over the skill infos and removes any sufficiently unused. */ class SkillInfoJanitorProcess extends TimerTask { /** Iterates over the skill infos and removes any sufficiently unused. */ @Override public void run() { synchronized (sessionDictionary) { final Iterator<SkillInfo> skillInfo_iter = sessionDictionary.values().iterator(); final long threshhold = System.currentTimeMillis() - cleanupDurationTimeMillis; while (skillInfo_iter.hasNext()) { final SkillInfo skillInfo = skillInfo_iter.next(); if (skillInfo.lastUsedTimeMillis < threshhold) { LOGGER.info("removing stale skill information session " + skillInfo.sessionHandle); skillInfo_iter.remove(); } } } } } /** Returns a string representation of this object. * * @return a string representation of this object */ @Override public String toString() { return "session managed " + skillClass.getSimpleName(); } /** Returns a list of the understood operations. * * @return a list of the understood operations */ @Override public String[] getUnderstoodOperations() { try { return ((AbstractSkill) skillClass.newInstance()).getUnderstoodOperations(); } catch (InstantiationException | IllegalAccessException ex) { throw new TexaiException(ex); } } /** Provides an informational container for a skill session. */ static class SkillInfo { /** the skill instance for a certain session */ private final AbstractSkill skill; /** the session handle */ private final String sessionHandle; /** the system millisecond timestamp of last use */ private long lastUsedTimeMillis; /** Constructs a new SkillInfo instance. * * @param skill the skill instance for a certain session * @param sessionHandle the session handle */ SkillInfo( final AbstractSkill skill, final String sessionHandle) { //Preconditions assert skill != null : "skill must not be null"; assert sessionHandle != null : "sessionHandle must not be null"; assert !sessionHandle.isEmpty() : "sessionHandle must not be empty"; this.skill = skill; this.sessionHandle = sessionHandle; lastUsedTimeMillis = System.currentTimeMillis(); } /** Returns a string representation of this object. * * @return a string representation of this object */ @Override public String toString() { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("["); stringBuilder.append(sessionHandle); stringBuilder.append(" "); stringBuilder.append(new Duration(lastUsedTimeMillis, System.currentTimeMillis())); stringBuilder.append("]"); return stringBuilder.toString(); } } }