net.cellcloud.talk.TalkService.java Source code

Java tutorial

Introduction

Here is the source code for net.cellcloud.talk.TalkService.java

Source

/*
-----------------------------------------------------------------------------
This source file is part of Cell Cloud.
    
Copyright (c) 2009-2014 Cell Cloud Team (www.cellcloud.net)
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------------------------------------------------------------------------
*/

package net.cellcloud.talk;

import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutorService;

import net.cellcloud.common.Cryptology;
import net.cellcloud.common.LogLevel;
import net.cellcloud.common.Logger;
import net.cellcloud.common.Message;
import net.cellcloud.common.NonblockingAcceptor;
import net.cellcloud.common.Packet;
import net.cellcloud.common.Service;
import net.cellcloud.common.Session;
import net.cellcloud.core.Cellet;
import net.cellcloud.core.CelletSandbox;
import net.cellcloud.core.Endpoint;
import net.cellcloud.core.Nucleus;
import net.cellcloud.core.NucleusContext;
import net.cellcloud.exception.InvalidException;
import net.cellcloud.exception.SingletonException;
import net.cellcloud.http.CookieSessionManager;
import net.cellcloud.http.HttpCapsule;
import net.cellcloud.http.HttpService;
import net.cellcloud.http.HttpSession;
import net.cellcloud.http.WebSocketManager;
import net.cellcloud.http.WebSocketSession;
import net.cellcloud.talk.dialect.ActionDialectFactory;
import net.cellcloud.talk.dialect.ChunkDialectFactory;
import net.cellcloud.talk.dialect.Dialect;
import net.cellcloud.talk.dialect.DialectEnumerator;
import net.cellcloud.talk.stuff.PrimitiveSerializer;
import net.cellcloud.util.CachedQueueExecutor;
import net.cellcloud.util.Utils;

import org.json.JSONException;
import org.json.JSONObject;

/** ??
 *
 * @author Jiangwei Xu
 */
public final class TalkService implements Service, SpeakerDelegate {

    private static TalkService instance = null;

    private int port;
    private int block;
    private int maxConnections;

    private long sessionTimeout;

    // ??? HTTP ?
    private boolean httpEnabled;
    private int httpPort;
    private int httpQueueSize;

    private CookieSessionManager httpSessionManager;
    private HttpSessionListener httpSessionListener;
    private long httpSessionTimeout;

    private WebSocketManager webSocketManager;

    private NonblockingAcceptor acceptor;
    private NucleusContext nucleusContext;
    private TalkAcceptorHandler talkHandler;

    // 
    protected ExecutorService executor;

    ///  Session
    private ConcurrentHashMap<Long, Certificate> unidentifiedSessions;
    /// Session  Tag 
    private ConcurrentHashMap<Long, String> sessionTagMap;
    /// Tag  Session context 
    private ConcurrentHashMap<String, TalkSessionContext> tagContexts;
    /// ?
    private ConcurrentHashMap<String, SuspendedTracker> suspendedTrackers;

    /// Tag 
    private ConcurrentSkipListSet<String> tagList;

    // ??? Speaker
    private ConcurrentHashMap<String, Speaker> speakerMap;
    protected Vector<Speaker> speakers;
    // HTTP ?? Speaker
    private ConcurrentHashMap<String, HttpSpeaker> httpSpeakerMap;
    protected Vector<HttpSpeaker> httpSpeakers;

    private TalkServiceDaemon daemon;
    private ArrayList<TalkListener> listeners;

    private CelletCallbackListener callbackListener;

    /** 
     * @throws SingletonException 
     */
    public TalkService(NucleusContext nucleusContext) throws SingletonException {
        if (null == TalkService.instance) {
            TalkService.instance = this;

            this.nucleusContext = nucleusContext;

            this.port = 7000;
            this.block = 16384;
            this.maxConnections = 1000;

            this.httpEnabled = true;
            this.httpPort = 7070;
            this.httpQueueSize = 1000;

            // 24 ?
            this.sessionTimeout = 24 * 60 * 60 * 1000;

            // 5 
            this.httpSessionTimeout = 5 * 60 * 1000;

            // 
            this.executor = CachedQueueExecutor.newCachedQueueThreadPool(8);

            // 
            DialectEnumerator.getInstance().addFactory(new ActionDialectFactory());
            DialectEnumerator.getInstance().addFactory(new ChunkDialectFactory());

            this.callbackListener = DialectEnumerator.getInstance();
        } else {
            throw new SingletonException(TalkService.class.getName());
        }
    }

    /**
     * ???
     */
    public static TalkService getInstance() {
        return TalkService.instance;
    }

    /**
     * ???
     * @return ?? true? false
     */
    @Override
    public boolean startup() {
        if (null == this.unidentifiedSessions) {
            this.unidentifiedSessions = new ConcurrentHashMap<Long, Certificate>();
        }
        if (null == this.sessionTagMap) {
            this.sessionTagMap = new ConcurrentHashMap<Long, String>();
        }
        if (null == this.tagContexts) {
            this.tagContexts = new ConcurrentHashMap<String, TalkSessionContext>();
        }
        if (null == this.suspendedTrackers) {
            this.suspendedTrackers = new ConcurrentHashMap<String, SuspendedTracker>();
        }
        if (null == this.tagList) {
            this.tagList = new ConcurrentSkipListSet<String>();
        }

        if (null == this.acceptor) {
            // ?
            this.acceptor = new NonblockingAcceptor();
            this.acceptor.setBlockSize(this.block);

            // 
            byte[] head = { 0x20, 0x10, 0x11, 0x10 };
            byte[] tail = { 0x19, 0x78, 0x10, 0x04 };
            this.acceptor.defineDataMark(head, tail);

            // ?
            this.talkHandler = new TalkAcceptorHandler(this);
            this.acceptor.setHandler(this.talkHandler);
        }

        // 
        this.acceptor.setMaxConnectNum(this.maxConnections);

        boolean succeeded = this.acceptor.bind(this.port);
        if (succeeded) {
            startDaemon();
        }

        if (succeeded && this.httpEnabled) {
            // ? HTTP ?
            startHttpService();
        }

        return succeeded;
    }

    /** ??
     */
    @Override
    public void shutdown() {
        if (null != this.acceptor) {
            this.acceptor.unbind();
        }

        stopDaemon();

        if (null != this.executor) {
            this.executor.shutdown();
        }

        if (this.httpEnabled && null != HttpService.getInstance()) {
            HttpService.getInstance().removeCapsule(this.httpPort);
        }

        if (null != this.tagContexts) {
            // ?
            Iterator<Map.Entry<String, TalkSessionContext>> iter = this.tagContexts.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, TalkSessionContext> entry = iter.next();
                TalkSessionContext ctx = entry.getValue();
                for (Cellet cellet : ctx.getTracker().getCelletList()) {
                    cellet.quitted(ctx.getTag());
                }
            }

            this.sessionTagMap.clear();
            this.tagContexts.clear();
            this.tagList.clear();
        }

        if (null != this.speakers) {
            for (Speaker speaker : this.speakers) {
                speaker.hangUp();
            }
            this.speakers.clear();
        }
        if (null != this.httpSpeakers) {
            for (HttpSpeaker speaker : this.httpSpeakers) {
                speaker.hangUp();
            }
            this.httpSpeakers.clear();
        }
        if (null != this.speakerMap) {
            this.speakerMap.clear();
        }
        if (null != this.httpSpeakerMap) {
            this.httpSpeakerMap.clear();
        }
    }

    /** ??
     * @note  startup ??
     * @param port ???
     */
    public void setPort(int port) {
        if (null != this.acceptor && this.acceptor.isRunning()) {
            throw new InvalidException("Can't set the port in talk service after the start");
        }

        this.port = port;
    }

    /** ??
     * @return
     */
    public int getPort() {
        return this.port;
    }

    /**
     * ???
     * @param size
     */
    public void setBlockSize(int size) {
        this.block = size;
    }

    /**
     * 
     * @param num 
     */
    public void setMaxConnections(int num) {
        if (null != this.acceptor && this.acceptor.isRunning()) {
            throw new InvalidException("Can't set the max number of connections in talk service after the start");
        }

        this.maxConnections = num;
    }

    /** ? HTTP ?
     */
    public void httpEnabled(boolean enabled) {
        if (null != HttpService.getInstance() && HttpService.getInstance().hasCapsule(this.httpPort)) {
            throw new InvalidException("Can't set the http enabled in talk service after the start");
        }

        this.httpEnabled = enabled;
    }

    /**  HTTP ??
     * @note  startup ??
     */
    public void setHttpPort(int port) {
        if (null != this.acceptor && this.acceptor.isRunning()) {
            throw new InvalidException("Can't set the http port in talk service after the start");
        }

        this.httpPort = port;
    }

    /**  HTTP ??
     * @return
     */
    public int getHttpPort() {
        return this.httpPort;
    }

    /**  HTTP ??
     * @param value
     */
    public void setHttpQueueSize(int value) {
        this.httpQueueSize = value;
    }

    /**  HTTP ?
     */
    public void settHttpSessionTimeout(long timeoutInMillisecond) {
        this.httpSessionTimeout = timeoutInMillisecond;
    }

    /** ?
     */
    public void startDaemon() {
        if (null == this.daemon) {
            this.daemon = new TalkServiceDaemon();
        }

        if (!this.daemon.running)
            this.daemon.start();
    }

    /** 
     */
    public void stopDaemon() {
        if (null != this.daemon) {
            this.daemon.stopSpinning();

            // 
            while (this.daemon.running) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Logger.log(TalkService.class, e, LogLevel.DEBUG);
                }
            }

            this.daemon = null;
        }
    }

    /** ??
     */
    public void addListener(TalkListener listener) {
        if (null == this.listeners) {
            this.listeners = new ArrayList<TalkListener>();
        }

        synchronized (this.listeners) {
            if (!this.listeners.contains(listener)) {
                this.listeners.add(listener);
            }
        }
    }

    /** ??
     */
    public void removeListener(TalkListener listener) {
        if (null == this.listeners) {
            return;
        }

        synchronized (this.listeners) {
            this.listeners.remove(listener);
        }
    }

    /**
     * ??
     * @param listener
     * @return
     */
    public boolean hasListener(TalkListener listener) {
        if (null == this.listeners) {
            return false;
        }

        synchronized (this.listeners) {
            return this.listeners.contains(listener);
        }
    }

    /**
     * ?? Tag 
     * @return
     */
    public Set<String> getTalkerList() {
        return this.tagList;
    }

    /**  Speaker 
     */
    public boolean notice(final String targetTag, final Primitive primitive, final Cellet cellet,
            final CelletSandbox sandbox) {
        //  Cellet ?
        if (!Nucleus.getInstance().checkSandbox(cellet, sandbox)) {
            Logger.w(TalkService.class, "Illegal cellet : " + cellet.getFeature().getIdentifier());
            return false;
        }

        TalkSessionContext context = this.tagContexts.get(targetTag);
        if (null == context) {
            if (Logger.isDebugLevel()) {
                Logger.w(TalkService.class, "Can't find target tag in context list : " + targetTag);
            }

            // ?
            this.tryOfferPrimitive(targetTag, cellet, primitive);

            // ?? false
            return false;
        }

        // ?
        if (this.tryOfferPrimitive(targetTag, cellet, primitive)) {
            // ?? false
            return false;
        }

        Message message = null;

        synchronized (context) {
            if (context.getTracker().hasCellet(cellet)) {

                if (null != this.callbackListener && primitive.isDialectal()) {
                    boolean ret = this.callbackListener.doTalk(cellet, targetTag, primitive.getDialect());
                    if (!ret) {
                        // ??
                        return true;
                    }
                }

                Session session = context.getLastSession();
                message = this.packetDialogue(cellet, primitive, (session instanceof WebSocketSession));
                if (null != message) {
                    session.write(message);
                }
            }
        }

        return (null != message);
    }

    /**  Speaker 
     */
    public boolean notice(final String targetTag, final Dialect dialect, final Cellet cellet,
            final CelletSandbox sandbox) {
        Primitive primitive = dialect.translate();
        if (null != primitive) {
            return this.notice(targetTag, primitive, cellet, sandbox);
        }
        return false;
    }

    /**  Cellet ?
     * 
     * @note Client
     */
    public boolean call(List<String> identifiers, InetSocketAddress address) {
        return this.call(identifiers, address, null, false);
    }

    /**
     *  Cellet ?
     * 
     * @param identifier
     * @param address
     * @return
     * 
     * @note Client
     */
    public boolean call(List<String> identifiers, InetSocketAddress address, boolean http) {
        return this.call(identifiers, address, null, http);
    }

    /**
     *  Cellet ?
     * 
     * @param identifier
     * @param address
     * @param capacity
     * @return
     * 
     * @note Client
     */
    public boolean call(List<String> identifiers, InetSocketAddress address, TalkCapacity capacity) {
        return this.call(identifiers, address, capacity, false);
    }

    /**  Cellet ?
     * 
     * @note Client
     */
    public synchronized boolean call(List<String> identifiers, InetSocketAddress address, TalkCapacity capacity,
            boolean http) {
        if (!http) {
            // ??? Speaker

            if (null == this.speakers) {
                this.speakers = new Vector<Speaker>();
                this.speakerMap = new ConcurrentHashMap<String, Speaker>();
            }

            for (String identifier : identifiers) {
                if (this.speakerMap.containsKey(identifier)) {
                    // ? Cellet??? Call
                    return false;
                }
            }

            //  Speaker
            Speaker speaker = new Speaker(address, this, this.block, capacity);
            this.speakers.add(speaker);

            // FIXME 28/11/14  call  Speaker ? Lost ? lost  true false
            // ?? Speaker ?
            //speaker.reset();

            for (String identifier : identifiers) {
                this.speakerMap.put(identifier, speaker);
            }

            // Call
            return speaker.call(identifiers);
        } else {
            // HTTP ?? Speaker

            // TODO
            if (null == this.httpSpeakers) {
                this.httpSpeakers = new Vector<HttpSpeaker>();
                this.httpSpeakerMap = new ConcurrentHashMap<String, HttpSpeaker>();
            }

            for (String identifier : identifiers) {
                if (this.httpSpeakerMap.containsKey(identifier)) {
                    // ? Cellet??? Call
                    return false;
                }
            }

            //  HttpSpeaker
            HttpSpeaker speaker = new HttpSpeaker(address, this, 30);
            this.httpSpeakers.add(speaker);

            for (String identifier : identifiers) {
                this.httpSpeakerMap.put(identifier, speaker);
            }

            // Call
            return speaker.call(identifiers);
        }
    }

    /**  Cellet 
     * 
     * @note Client
     */
    public void suspend(final String identifier, final long duration) {
        if (null == this.speakerMap || !this.speakerMap.containsKey(identifier))
            return;

        Speaker speaker = this.speakerMap.get(identifier);
        if (null != speaker) {
            speaker.suspend(duration);
        }
    }

    /** ?? Cellet 
     * 
     * @note Client
     */
    public void resume(final String identifier, final long startTime) {
        if (null == this.speakerMap || !this.speakerMap.containsKey(identifier))
            return;

        Speaker speaker = this.speakerMap.get(identifier);
        if (null != speaker) {
            speaker.resume(startTime);
        }
    }

    /**  Cellet ?
     * 
     * @note Client
     */
    public void hangUp(List<String> identifiers) {
        if (null != this.speakerMap) {
            for (String identifier : identifiers) {
                if (this.speakerMap.containsKey(identifier)) {
                    Speaker speaker = this.speakerMap.remove(identifier);

                    for (String celletIdentifier : speaker.getIdentifiers()) {
                        this.speakerMap.remove(celletIdentifier);
                    }

                    this.speakers.remove(speaker);

                    speaker.hangUp();
                }
            }
        }

        if (null != this.httpSpeakerMap) {
            for (String identifier : identifiers) {
                if (this.httpSpeakerMap.containsKey(identifier)) {
                    HttpSpeaker speaker = this.httpSpeakerMap.remove(identifier);

                    for (String celletIdentifier : speaker.getIdentifiers()) {
                        this.httpSpeakerMap.remove(celletIdentifier);
                    }

                    this.httpSpeakers.remove(speaker);

                    speaker.hangUp();
                }
            }
        }
    }

    /** ? Cellet ??
     * 
     * @note Client
     */
    public boolean talk(final String identifier, final Primitive primitive) {
        if (null != this.speakerMap) {
            Speaker speaker = this.speakerMap.get(identifier);
            if (null != speaker) {
                // Speak
                return speaker.speak(identifier, primitive);
            }
        }

        if (null != this.httpSpeakerMap) {
            HttpSpeaker speaker = this.httpSpeakerMap.get(identifier);
            if (null != speaker) {
                // Speak
                return speaker.speak(identifier, primitive);
            }
        }

        return false;
    }

    /** ? Cellet ??
     * 
     * @note Client
     */
    public boolean talk(final String identifier, final Dialect dialect) {
        if (null == this.speakerMap && null == this.httpSpeakerMap)
            return false;

        Primitive primitive = dialect.translate();
        if (null != primitive) {
            return this.talk(identifier, primitive);
        }

        return false;
    }

    /** ?? Cellet ?
     * 
     * @note Client
     */
    public boolean isCalled(final String identifier) {
        if (null != this.speakerMap) {
            Speaker speaker = this.speakerMap.get(identifier);
            if (null != speaker) {
                return speaker.isCalled();
            }
        }

        if (null != this.httpSpeakerMap) {
            HttpSpeaker speaker = this.httpSpeakerMap.get(identifier);
            if (null != speaker) {
                return speaker.isCalled();
            }
        }

        return false;
    }

    /** Cellet ???
     * 
     * @note Client
     */
    public boolean isSuspended(final String identifier) {
        if (null == this.speakerMap)
            return false;

        Speaker speaker = this.speakerMap.get(identifier);
        if (null != speaker) {
            return speaker.isSuspended();
        }

        return false;
    }

    public ExecutorService getExecutor() {
        return this.executor;
    }

    /** ? HTTP ?
     */
    private void startHttpService() {
        if (null == HttpService.getInstance()) {
            Logger.w(TalkService.class, "Starts talk http service failed, http service is not started.");
            return;
        }

        this.webSocketManager = HttpService.getInstance().activeWebSocket(this.httpPort + 1, this.httpQueueSize,
                new WebSocketMessageHandler(this));

        //  Session ?
        this.httpSessionManager = new CookieSessionManager();

        // ?
        this.httpSessionListener = new HttpSessionListener();
        this.httpSessionManager.addSessionListener(this.httpSessionListener);

        // ?
        HttpCapsule capsule = new HttpCapsule(this.httpPort, this.httpQueueSize);
        //  Session ?
        capsule.setSessionManager(this.httpSessionManager);
        // ? Holder 
        capsule.addHolder(new HttpInterrogationHandler(this));
        capsule.addHolder(new HttpCheckHandler(this));
        capsule.addHolder(new HttpRequestHandler(this));
        capsule.addHolder(new HttpDialogueHandler(this));
        capsule.addHolder(new HttpHeartbeatHandler());

        //  HTTP ?
        HttpService.getInstance().addCapsule(capsule);
    }

    /**
     *  Dialogue 
     */
    @Override
    public void onDialogue(Speakable speaker, String identifier, Primitive primitive) {
        if (null == this.listeners) {
            return;
        }

        synchronized (this.listeners) {
            for (TalkListener listener : this.listeners) {
                listener.dialogue(identifier, primitive);
            }
        }
    }

    /**
     * 
     */
    @Override
    public void onContacted(Speakable speaker, String identifier) {
        if (null == this.listeners) {
            return;
        }

        String tag = speaker.getRemoteTag();
        synchronized (this.listeners) {
            for (TalkListener listener : this.listeners) {
                listener.contacted(identifier, tag);
            }
        }
    }

    /**
     * 
     */
    @Override
    public void onQuitted(Speakable speaker, String identifier) {
        if (null == this.listeners) {
            return;
        }

        String tag = speaker.getRemoteTag();
        synchronized (this.listeners) {
            for (TalkListener listener : this.listeners) {
                listener.quitted(identifier, tag);
            }
        }
    }

    /**
     * 
     */
    @Override
    public void onSuspended(Speakable speaker, long timestamp, int mode) {
        if (null == this.listeners) {
            return;
        }

        String tag = speaker.getRemoteTag();
        synchronized (this.listeners) {
            for (TalkListener listener : this.listeners) {
                listener.suspended(tag, timestamp, mode);
            }
        }
    }

    /**
     * ??
     */
    @Override
    public void onResumed(Speakable speaker, long timestamp, Primitive primitive) {
        if (null == this.listeners) {
            return;
        }

        String tag = speaker.getRemoteTag();
        synchronized (this.listeners) {
            for (TalkListener listener : this.listeners) {
                listener.resumed(tag, timestamp, primitive);
            }
        }
    }

    /**
     * ?
     */
    @Override
    public void onFailed(Speakable speaker, TalkServiceFailure failure) {
        if (null == this.listeners) {
            return;
        }

        String tag = speaker.getRemoteTag();
        synchronized (this.listeners) {
            for (TalkListener listener : this.listeners) {
                listener.failed(tag, failure);
            }
        }
    }

    /**
     *  Endpoint 
     * @param remoteTag
     * @return
     */
    public Endpoint findEndpoint(String remoteTag) {
        TalkSessionContext ctx = this.tagContexts.get(remoteTag);
        if (null != ctx) {
            return ctx.getEndpoint();
        }

        return null;
    }

    /** ? Session 
     */
    protected synchronized Certificate openSession(Session session) {
        Long sid = session.getId();
        if (this.unidentifiedSessions.containsKey(sid)) {
            return this.unidentifiedSessions.get(sid);
        }

        Certificate cert = new Certificate();
        cert.session = session;
        cert.key = Utils.randomString(8);
        cert.plaintext = Utils.randomString(16);
        this.unidentifiedSessions.put(sid, cert);

        return cert;
    }

    /**  Session 
     */
    protected synchronized void closeSession(final Session session) {
        String tag = this.sessionTagMap.get(session.getId());
        if (null != tag) {
            TalkSessionContext ctx = this.tagContexts.get(tag);
            if (null != ctx) {
                TalkTracker tracker = ctx.getTracker();

                // ??
                if (tracker.isAutoSuspend()) {
                    // 
                    this.suspendTalk(ctx, SuspendMode.PASSIVE);
                    for (Cellet cellet : tracker.getCelletList()) {
                        //  Cellet 
                        cellet.suspended(tag);
                    }
                } else if (this.suspendedTrackers.containsKey(tag)) {
                    // ?? Cellet 
                    for (Cellet cellet : tracker.getCelletList()) {
                        SuspendedTracker st = this.suspendedTrackers.get(tag);
                        if (!st.exist(cellet)) {
                            // 
                            cellet.quitted(tag);
                        } else {
                            // 
                            // FIXME 01/01/2013 ???
                            // ???
                        }
                    }
                } else {
                    // ?

                    ctx.removeSession(session);

                    if (ctx.numSessions() == 0) {
                        for (Cellet cellet : tracker.getCelletList()) {
                            cellet.quitted(tag);
                        }

                        Logger.i(this.getClass(), "Clear session: " + tag);

                        // ?
                        this.tagContexts.remove(tag);
                        this.tagList.remove(tag);
                    }
                }
            }

            this.sessionTagMap.remove(session.getId());
        } else {
            //
            Logger.i(this.getClass(), "Can NOT find tag with session: " + session.getAddress().getHostString());
        }

        // ??
        this.unidentifiedSessions.remove(session.getId());
    }

    /** ? Session 
     */
    protected synchronized void acceptSession(Session session, String tag) {
        Long sid = session.getId();
        this.unidentifiedSessions.remove(sid);

        // Session -> Tag
        this.sessionTagMap.put(session.getId(), tag);

        // Tag -> Context
        TalkSessionContext ctx = this.tagContexts.get(tag);
        if (null != ctx) {
            //  Tag  session
            ctx.addSession(session);
        } else {
            // 
            ctx = new TalkSessionContext(tag, session);
            ctx.dialogueTickTime = this.getTickTime();
            this.tagContexts.put(tag, ctx);
        }

        //  Tag
        if (!this.tagList.contains(tag)) {
            this.tagList.add(tag);
        }
    }

    /** ? Session 
     */
    protected synchronized void rejectSession(Session session) {
        Long sid = session.getId();

        StringBuilder log = new StringBuilder();
        log.append("Talk service reject session (");
        log.append(sid);
        log.append("): ");
        log.append(session.getAddress().getAddress().getHostAddress());
        log.append(":");
        log.append(session.getAddress().getPort());
        Logger.w(TalkService.class, log.toString());
        log = null;

        this.unidentifiedSessions.remove(sid);

        //  Tag context
        String tag = this.sessionTagMap.remove(sid);
        if (null != tag) {
            TalkSessionContext ctx = this.tagContexts.get(tag);
            if (null != ctx) {
                ctx.removeSession(session);
            }
        }

        if (!(session instanceof HttpSession) && !(session instanceof WebSocketSession)) {
            this.acceptor.close(session);
        } else if (session instanceof WebSocketSession) {
            this.webSocketManager.close((WebSocketSession) session);
        }
    }

    /**  Cellet 
     */
    protected TalkTracker processRequest(Session session, String tag, String identifier) {
        TalkSessionContext ctx = this.tagContexts.get(tag);
        if (null == ctx) {
            return null;
        }

        final Cellet cellet = Nucleus.getInstance().getCellet(identifier, this.nucleusContext);

        TalkTracker tracker = null;

        if (null != cellet) {
            tracker = ctx.getTracker();
            if (!tracker.hasCellet(cellet)) {
                tracker.addCellet(cellet);
            }

            // ??? Talk
            if (this.tryResumeTalk(tag, cellet, SuspendMode.PASSIVE, 0)) {
                //  resumed
                cellet.resumed(tag);
            } else {
                //  contacted
                cellet.contacted(tag);
            }
        }

        return tracker;
    }

    /** ???
     */
    protected TalkCapacity processConsult(Session session, String tag, TalkCapacity capacity) {
        TalkSessionContext ctx = this.tagContexts.get(tag);
        if (null == ctx) {
            return new TalkCapacity(false, 0);
        }

        TalkTracker tracker = ctx.getTracker();

        // ??
        tracker.setAutoSuspend(capacity.autoSuspend);
        // 
        tracker.setSuspendDuration(capacity.suspendDuration);

        return new TalkCapacity(tracker.isAutoSuspend(), tracker.getSuspendDuration());
    }

    /** ? Cellet 
     */
    protected void processDialogue(Session session, String speakerTag, String targetIdentifier,
            Primitive primitive) {
        TalkSessionContext ctx = this.tagContexts.get(speakerTag);
        if (null != ctx) {
            ctx.dialogueTickTime = this.getTickTime();

            TalkTracker tracker = ctx.getTracker();
            Cellet cellet = tracker.getCellet(targetIdentifier);
            if (null != cellet) {
                primitive.setCelletIdentifier(cellet.getFeature().getIdentifier());
                primitive.setCellet(cellet);

                if (null != this.callbackListener && primitive.isDialectal()) {
                    boolean ret = this.callbackListener.doDialogue(cellet, speakerTag, primitive.getDialect());
                    if (!ret) {
                        // ?
                        return;
                    }
                }

                //  Cellet
                cellet.dialogue(speakerTag, primitive);
            }
        }
    }

    /** ?
     */
    protected boolean processSuspend(Session session, String speakerTag, long duration) {
        TalkSessionContext ctx = this.tagContexts.get(speakerTag);
        if (null == ctx) {
            return false;
        }

        TalkTracker talkTracker = ctx.getTracker();
        // 
        SuspendedTracker st = this.suspendTalk(ctx, SuspendMode.INITATIVE);
        if (null != st) {
            // 
            st.liveDuration = duration;

            //  Cellet ?
            for (Cellet cellet : talkTracker.getCelletList()) {
                cellet.suspended(speakerTag);
            }

            return true;
        }

        return false;
    }

    /** ???
     */
    protected void processResume(Session session, String speakerTag, long startTime) {
        TalkSessionContext ctx = this.tagContexts.get(session);
        if (null == ctx) {
            return;
        }

        TalkTracker talkTracker = ctx.getTracker();
        if (talkTracker.getCelletList().isEmpty()) {
            return;
        }

        // ??
        for (Cellet cellet : talkTracker.getCelletList()) {
            if (this.tryResumeTalk(speakerTag, cellet, SuspendMode.INITATIVE, startTime)) {
                // ??
                cellet.resumed(speakerTag);
            }
        }
    }

    /** ???
     */
    protected void noticeResume(Cellet cellet, String targetTag, Queue<Long> timestampQueue,
            Queue<Primitive> primitiveQueue, long startTime) {
        TalkSessionContext context = this.tagContexts.get(targetTag);
        if (null == context) {
            if (Logger.isDebugLevel()) {
                Logger.d(TalkService.class, "Not find session by remote tag");
            }
            return;
        }

        Message message = null;

        synchronized (context) {
            // ?
            TalkTracker tracker = context.getTracker();
            // ?? Cellet
            if (tracker.getCellet(cellet.getFeature().getIdentifier()) == cellet) {
                Session session = context.getLastSession();

                // ??
                for (int i = 0, size = timestampQueue.size(); i < size; ++i) {
                    Long timestamp = timestampQueue.poll();
                    Primitive primitive = primitiveQueue.poll();
                    if (timestamp.longValue() >= startTime) {
                        message = this.packetResume(targetTag, timestamp, primitive);
                        if (null != message) {
                            session.write(message);
                        }
                    }
                }
            }
        }
    }

    /**  Session ?
     */
    protected Certificate getCertificate(Session session) {
        return this.unidentifiedSessions.get(session.getId());
    }

    /** ? Session 
     */
    protected void processUnidentifiedSessions(long time) {
        if (null == this.unidentifiedSessions || this.unidentifiedSessions.isEmpty()) {
            return;
        }

        //  Session
        ArrayList<Session> sessionList = null;

        Iterator<Map.Entry<Long, Certificate>> iter = this.unidentifiedSessions.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<Long, Certificate> e = iter.next();
            Certificate cert = e.getValue();
            if (false == cert.checked) {
                cert.checked = true;
                deliverChecking(cert.session, cert.plaintext, cert.key);
            } else {
                // 20 
                if (time - cert.time > 20000) {
                    if (null == sessionList) {
                        sessionList = new ArrayList<Session>();
                    }

                    sessionList.add(cert.session);
                }
            }
        }

        if (null != sessionList) {
            //  Session
            Iterator<Session> siter = sessionList.iterator();
            while (siter.hasNext()) {
                Session session = siter.next();

                StringBuilder log = new StringBuilder();
                log.append("Talk service session timeout: ");
                log.append(session.getAddress().getAddress().getHostAddress());
                log.append(":");
                log.append(session.getAddress().getPort());
                Logger.i(TalkService.class, log.toString());
                log = null;

                // 
                this.unidentifiedSessions.remove(session.getId());

                if (session instanceof HttpSession) {
                    //  HTTP  Session
                    this.httpSessionManager.unmanage((HttpSession) session);
                } else if (session instanceof WebSocketSession) {
                    //  WebSocket  Session
                    this.webSocketManager.close((WebSocketSession) session);
                } else {
                    // ??? Session
                    this.acceptor.close(session);
                }
            }

            sessionList.clear();
            sessionList = null;
        }
    }

    /**  Session tick time 
     */
    protected void updateSessionHeartbeat(Session session) {
        String tag = this.sessionTagMap.get(session.getId());
        if (null == tag) {
            return;
        }

        TalkSessionContext ctx = this.tagContexts.get(tag);
        if (null != ctx) {
            ctx.updateSessionHeartbeat(session, this.getTickTime());

            if (Logger.isDebugLevel()) {
                Logger.d(this.getClass(),
                        "Talk service heartbeat from " + session.getAddress().getAddress().getHostAddress() + ":"
                                + session.getAddress().getPort());
            }
        }
    }

    /** 
     */
    protected long getTickTime() {
        return this.daemon.getTickTime();
    }

    /**
     * ??
     * @param timeout
     */
    protected void checkSessionHeartbeat() {
        if (null == this.tagContexts) {
            return;
        }

        LinkedList<Session> closeList = new LinkedList<Session>();

        for (Map.Entry<String, TalkSessionContext> entry : this.tagContexts.entrySet()) {
            TalkSessionContext ctx = entry.getValue();

            List<Session> sl = ctx.getSessions();
            for (Session s : sl) {
                long time = ctx.getSessionHeartbeat(s);
                if (time == 0) {
                    continue;
                }

                if (this.daemon.getTickTime() - time > this.sessionTimeout) {
                    //  Session 
                    closeList.add(s);
                    Logger.d(this.getClass(), "Session timeout in heartbeat: " + s.getAddress().getHostString());
                }
            }
        }

        for (Session session : closeList) {
            this.closeSession(session);
        }

        closeList.clear();
        closeList = null;
    }

    /**  HTTP Session 
     */
    protected void checkHttpSessionHeartbeat() {
        if (null == this.httpSessionManager) {
            return;
        }

        this.executor.execute(new Runnable() {
            @Override
            public void run() {
                long time = daemon.getTickTime();
                List<HttpSession> list = httpSessionManager.getSessions();
                for (HttpSession session : list) {
                    if (time - session.getHeartbeat() > httpSessionTimeout) {
                        httpSessionManager.unmanage(session);
                    }
                }
            }
        });
    }

    /** ?
     */
    protected void checkAndDeleteSuspendedTalk() {
        if (null == this.suspendedTrackers) {
            return;
        }

        // ????
        // 1??
        // 2??? Cellet ?

        // ?
        Iterator<Map.Entry<String, SuspendedTracker>> eiter = this.suspendedTrackers.entrySet().iterator();
        while (eiter.hasNext()) {
            Map.Entry<String, SuspendedTracker> entry = eiter.next();
            SuspendedTracker tracker = entry.getValue();
            if (tracker.isTimeout()) {
                // ??? Cellet 
                if (!this.tagContexts.containsKey(tracker.getTag())) {
                    // 
                    List<Cellet> list = tracker.getCelletList();
                    for (Cellet cellet : list) {
                        cellet.quitted(entry.getKey());
                    }
                }

                // 
                eiter.remove();
            }
        }
    }

    /** ?
     */
    private SuspendedTracker suspendTalk(TalkSessionContext ctx, int suspendMode) {
        if (this.suspendedTrackers.containsKey(ctx.getTag())) {
            SuspendedTracker tracker = this.suspendedTrackers.get(ctx.getTag());
            for (Cellet cellet : ctx.getTracker().getCelletList()) {
                tracker.track(cellet, suspendMode);
            }
            return tracker;
        }

        SuspendedTracker tracker = new SuspendedTracker(ctx.getTag());
        for (Cellet cellet : ctx.getTracker().getCelletList()) {
            tracker.track(cellet, suspendMode);
        }
        tracker.liveDuration = ctx.getTracker().getSuspendDuration();
        this.suspendedTrackers.put(ctx.getTag(), tracker);
        return tracker;
    }

    /** ????
     */
    private synchronized boolean tryResumeTalk(String tag, Cellet cellet, int suspendMode, long startTime) {
        SuspendedTracker tracker = this.suspendedTrackers.get(tag);
        if (null != tracker) {
            boolean ret = tracker.pollPrimitiveMatchMode(this.executor, cellet, suspendMode, startTime);
            if (ret) {
                tracker.retreat(cellet);
                return true;
            }
        }

        return false;
    }

    /** ??
     */
    private boolean tryOfferPrimitive(String tag, Cellet cellet, Primitive primitive) {
        SuspendedTracker tracker = this.suspendedTrackers.get(tag);
        if (null != tracker) {
            tracker.offerPrimitive(cellet, System.currentTimeMillis(), primitive);
            return true;
        }

        return false;
    }

    /** ? Session ??
     */
    private void deliverChecking(Session session, String text, String key) {
        // ? HTTP ?? Session 
        if (session instanceof HttpSession) {
            return;
        }

        // ? WebSocket  Session
        if (session instanceof WebSocketSession) {
            byte[] ciphertext = Cryptology.getInstance().simpleEncrypt(text.getBytes(), key.getBytes());
            JSONObject data = new JSONObject();
            try {
                JSONObject packet = new JSONObject();
                // {"ciphertext": ciphertext, "key": key}
                packet.put(HttpInterrogationHandler.Ciphertext, Cryptology.getInstance().encodeBase64(ciphertext));
                packet.put(HttpInterrogationHandler.Key, key);

                data.put(WebSocketMessageHandler.TALK_PACKET_TAG, WebSocketMessageHandler.TPT_INTERROGATE);
                data.put(WebSocketMessageHandler.TALK_PACKET, packet);
            } catch (JSONException e) {
                Logger.log(TalkService.class, e, LogLevel.ERROR);
            }

            Message message = new Message(data.toString());
            this.webSocketManager.write((WebSocketSession) session, message);
            message = null;
            return;
        }

        // ?|

        byte[] ciphertext = Cryptology.getInstance().simpleEncrypt(text.getBytes(), key.getBytes());

        Packet packet = new Packet(TalkDefinition.TPT_INTERROGATE, 1, 1, 0);
        packet.appendSubsegment(ciphertext);
        packet.appendSubsegment(key.getBytes());

        byte[] data = Packet.pack(packet);
        if (null != data) {
            Message message = new Message(data);
            this.acceptor.write(session, message);
            message = null;
        }

        packet = null;
    }

    private Message packetResume(String targetTag, Long timestamp, Primitive primitive) {
        // ?||?

        // ?
        ByteArrayOutputStream stream = primitive.write();

        // ??
        Packet packet = new Packet(TalkDefinition.TPT_RESUME, 6, 1, 0);
        packet.appendSubsegment(Utils.string2Bytes(targetTag));
        packet.appendSubsegment(Utils.string2Bytes(timestamp.toString()));
        packet.appendSubsegment(stream.toByteArray());

        // ?
        byte[] data = Packet.pack(packet);
        Message message = new Message(data);
        return message;
    }

    /** ?
     */
    private Message packetDialogue(Cellet cellet, Primitive primitive, boolean jsonFormat) {
        Message message = null;

        if (jsonFormat) {
            try {
                JSONObject primJson = new JSONObject();
                PrimitiveSerializer.write(primJson, primitive);

                JSONObject packet = new JSONObject();
                packet.put(HttpDialogueHandler.Primitive, primJson);
                packet.put(HttpDialogueHandler.Identifier, cellet.getFeature().getIdentifier());

                JSONObject data = new JSONObject();
                data.put(WebSocketMessageHandler.TALK_PACKET_TAG, WebSocketMessageHandler.TPT_DIALOGUE);
                data.put(WebSocketMessageHandler.TALK_PACKET, packet);

                //  message
                message = new Message(data.toString());
            } catch (JSONException e) {
                Logger.log(this.getClass(), e, LogLevel.ERROR);
            }
        } else {
            // ??|Cellet

            // ?
            ByteArrayOutputStream stream = primitive.write();

            // ??
            Packet packet = new Packet(TalkDefinition.TPT_DIALOGUE, 99, 1, 0);
            packet.appendSubsegment(stream.toByteArray());
            packet.appendSubsegment(Utils.string2Bytes(cellet.getFeature().getIdentifier()));

            // ?
            byte[] data = Packet.pack(packet);
            message = new Message(data);
        }

        return message;
    }

    /** ??
    */
    protected class Certificate {
        /**  */
        protected Certificate() {
            this.session = null;
            this.key = null;
            this.plaintext = null;
            this.time = System.currentTimeMillis();
            this.checked = false;
        }

        ///  Session
        protected Session session;
        /// 
        protected String key;
        /// 
        protected String plaintext;
        /// 
        protected long time;
        /// ????
        protected boolean checked;
    }
}