edu.cuny.cat.server.ConnectionAdaptor.java Source code

Java tutorial

Introduction

Here is the source code for edu.cuny.cat.server.ConnectionAdaptor.java

Source

/*
 * JCAT - TAC Market Design Competition Platform
 * Copyright (C) 2006-2010 Jinzhong Niu, Kai Cai
 *
 * 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.
 */

package edu.cuny.cat.server;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

import org.apache.commons.collections15.Buffer;
import org.apache.commons.collections15.BufferUtils;
import org.apache.commons.collections15.buffer.UnboundedFifoBuffer;
import org.apache.log4j.Logger;

import edu.cuny.cat.comm.CatException;
import edu.cuny.cat.comm.CatpMessage;
import edu.cuny.cat.comm.CatpMessageErrorException;
import edu.cuny.cat.comm.CatpMessageException;
import edu.cuny.cat.comm.CatpMessageInvalidException;
import edu.cuny.cat.comm.CatpProactiveSession;
import edu.cuny.cat.comm.CatpReactiveSession;
import edu.cuny.cat.comm.CatpRequest;
import edu.cuny.cat.comm.CatpResponse;
import edu.cuny.cat.comm.Connection;
import edu.cuny.cat.comm.ConnectionException;
import edu.cuny.cat.comm.ConnectionListener;
import edu.cuny.cat.comm.ListenableConnection;
import edu.cuny.cat.comm.MessageHandler;
import edu.cuny.cat.comm.ReactiveConnection;
import edu.cuny.cat.comm.Session;
import edu.cuny.cat.comm.TimableCatpProactiveSession;
import edu.cuny.cat.core.AccountHolder;
import edu.cuny.cat.core.IllegalShoutException;
import edu.cuny.cat.core.IllegalShoutInTransactionException;
import edu.cuny.cat.core.IllegalTransactionException;
import edu.cuny.cat.core.IllegalTransactionPriceException;
import edu.cuny.cat.core.InvalidChargeException;
import edu.cuny.cat.core.Shout;
import edu.cuny.cat.core.Specialist;
import edu.cuny.cat.core.Trader;
import edu.cuny.cat.core.Transaction;
import edu.cuny.cat.event.AuctionEvent;
import edu.cuny.cat.event.AuctionEventListener;
import edu.cuny.cat.event.AvailableMarketsAnnouncedEvent;
import edu.cuny.cat.event.AvailableTradersAnnouncedEvent;
import edu.cuny.cat.event.ClientStateUpdatedEvent;
import edu.cuny.cat.event.DayClosedEvent;
import edu.cuny.cat.event.DayOpenedEvent;
import edu.cuny.cat.event.DayOpeningEvent;
import edu.cuny.cat.event.FeesAnnouncedEvent;
import edu.cuny.cat.event.GameOverEvent;
import edu.cuny.cat.event.GameStartedEvent;
import edu.cuny.cat.event.GameStartingEvent;
import edu.cuny.cat.event.PrivateValueAssignedEvent;
import edu.cuny.cat.event.ProfitAnnouncedEvent;
import edu.cuny.cat.event.RegistrationEvent;
import edu.cuny.cat.event.RoundClosedEvent;
import edu.cuny.cat.event.RoundClosingEvent;
import edu.cuny.cat.event.RoundOpenedEvent;
import edu.cuny.cat.event.ShoutPlacedEvent;
import edu.cuny.cat.event.ShoutPostedEvent;
import edu.cuny.cat.event.ShoutReceivedEvent;
import edu.cuny.cat.event.ShoutRejectedEvent;
import edu.cuny.cat.event.SimulationOverEvent;
import edu.cuny.cat.event.SimulationStartedEvent;
import edu.cuny.cat.event.SpecialistCheckInEvent;
import edu.cuny.cat.event.SubscriptionEvent;
import edu.cuny.cat.event.TraderCheckInEvent;
import edu.cuny.cat.event.TransactionExecutedEvent;
import edu.cuny.cat.event.TransactionPostedEvent;
import edu.cuny.cat.registry.Registry;
import edu.cuny.cat.task.EventDispatchingTaskOnServerSide;
import edu.cuny.cat.task.IncomingMessageDispatchingTask;
import edu.cuny.cat.task.OutgoingMessageDispatchingTask;
import edu.cuny.cat.valuation.ValuationPolicy;
import edu.cuny.event.Event;
import edu.cuny.event.EventEngine;
import edu.cuny.util.Galaxy;

/**
 * Each instance of this class is created by {@link ConnectionManager} on behalf
 * of a game server to deal with requests and responses from/to a game client.
 * 
 * @author Jinzhong Niu
 * @version $Revision: 1.145 $
 */
public final class ConnectionAdaptor
        implements AuctionEventListener, ConnectionListener<CatpMessage>, MessageHandler<CatpMessage>, Observer {

    protected static Logger logger = Logger.getLogger(ConnectionAdaptor.class);

    protected EventEngine eventEngine;

    protected ConnectionManager manager;

    protected GameController controller;

    protected ReactiveConnection<CatpMessage> connection;

    protected Registry registry;

    protected GameClock clock;

    protected ShoutValidator shoutValidator;

    protected TransactionValidator transactionValidator;

    protected ChargeValidator chargeValidator;

    protected TimeoutController timeController;

    protected Buffer<CatpProactiveSession> proactiveSessions;

    protected CatpReactiveSession reactiveSessions[];

    /*
     * stores pending sessions for shouts, registration and subscription requests
     * key is shout id, or REGISTER/SUBSCRIBE + specialist.id.
     * 
     * TODO: currently only shouts involve pending sessions.
     */
    protected Map<String, ShoutFromTraderSession> pendingRequestSessions;

    protected AccountHolder client;

    protected ValuationPolicy valuer;

    protected int state;

    public static String tag = "";

    public ConnectionAdaptor(final ConnectionManager manager, final Connection<CatpMessage> conn) {

        eventEngine = Galaxy.getInstance().getDefaultTyped(EventEngine.class);

        this.manager = manager;

        controller = GameController.getInstance();

        clock = controller.getClock();
        shoutValidator = controller.getShoutValidator();
        transactionValidator = controller.getTransactionValidator();
        chargeValidator = controller.getChargeValidator();
        registry = controller.getRegistry();
        timeController = controller.getTimeController();

        proactiveSessions = BufferUtils.synchronizedBuffer(new UnboundedFifoBuffer<CatpProactiveSession>());
        pendingRequestSessions = Collections.synchronizedMap(new HashMap<String, ShoutFromTraderSession>());

        connection = ListenableConnection.makeReactiveConnection(conn);

        setExpectedReactiveSessions(
                new CatpReactiveSession[] { new CheckInSession(), new OracleSession("BeforeCheckIn") });

        openConnection();

        setState(ClientState.READY, ClientState.getCodeDesc(ClientState.READY) + " for checking in");
    }

    private void openConnection() {
        connection.setListener(this);

        try {
            connection.open();
        } catch (final ConnectionException e) {
            e.printStackTrace();
            ConnectionAdaptor.logger.fatal(e.toString(), e);
            setState(ClientState.FATAL, ClientState.getCodeDesc(ClientState.FATAL) + " in openning connection");
            return;
        }
    }

    public AccountHolder getClient() {
        return client;
    }

    public boolean isTrader() {
        return (client instanceof Trader);
    }

    public boolean isSeller() {
        return isTrader() && ((Trader) client).isSeller();
    }

    public boolean isSpecialist() {
        return (client instanceof Specialist);
    }

    public void setValuer(final ValuationPolicy valuer) {
        this.valuer = valuer;
    }

    public ValuationPolicy getValuer() {
        return valuer;
    }

    protected void processGameStarting(final GameStartingEvent event) {

        /*
         * NOTE: any existing pending proactive session may indicate some
         * improvement might be needed.
         */
        clearPendingProactiveSessions();

        setExpectedReactiveSessions(new CatpReactiveSession[] { new OracleSession("GameStarting") });

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.OPTIONS,
                new String[] { CatpMessage.TYPE, CatpMessage.GAMESTARTING, CatpMessage.VALUE,
                        CatpMessage.concatenate(new int[] { clock.getDayLen(), clock.getRoundLen() }) });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new GameStartingSession(request);
        startProactiveSession(session);
    }

    protected void processAvailableTradersAnnounced(final AvailableTradersAnnouncedEvent event) {

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.POST,
                new String[] { CatpMessage.TYPE, CatpMessage.TRADER, CatpMessage.ID,
                        // send list of all traders including failed one in case some that
                        // failed after checkin may reconnect.
                        // CatpMessage.concatenate(registry.getWorkingTraderIds()) });
                        CatpMessage.concatenate(registry.getTraderIds()) });
        request.setTrigger(event);
        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new PostTraderSession(request);
        startProactiveSession(session);
    }

    protected void processAvailableMarketsAnnounced(final AvailableMarketsAnnouncedEvent event) {
        final CatpRequest request = CatpRequest.createRequest(CatpMessage.POST,
                new String[] { CatpMessage.TYPE, CatpMessage.SPECIALIST, CatpMessage.ID,
                        // send list of all specialists including failed ones in case some
                        // that failed after checkin may reconnect.
                        // CatpMessage.concatenate(registry.getWorkingSpecialistIds()) });
                        CatpMessage.concatenate(registry.getSpecialistIds()) });

        request.setTrigger(event);
        request.setTag(ConnectionAdaptor.tag);

        final PostSpecialistSession session = new PostSpecialistSession(request);
        startProactiveSession(session);

    }

    protected void processGameStarted(final GameStartedEvent event) {

        setExpectedReactiveSessions(new CatpReactiveSession[] { new OracleSession("GameStarted") });

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.OPTIONS,
                new String[] { CatpMessage.TYPE, CatpMessage.GAMESTARTED });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new GameStartedSession(request);
        startProactiveSession(session);
    }

    protected void processGameOver(final GameOverEvent event) {
        setExpectedReactiveSessions(new CatpReactiveSession[] { new GetTraderSession(), new GetSpecialistSession(),
                new GetFeeSession(), new GetProfitSession(), new OracleSession("GameOver") });

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.OPTIONS,
                new String[] { CatpMessage.TYPE, CatpMessage.GAMEOVER });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new GameOverSession(request);
        startProactiveSession(session);
    }

    protected void processDayOpening(final DayOpeningEvent event) {

        if (isTrader()) {
            final Trader trader = (Trader) client;
            if (trader.getSpecialistId() != null) {
                ConnectionAdaptor.logger.error("Failed in asserting the unregistered status of trader "
                        + getClientId() + " at day " + event.getDay() + " !");
            }
        } else {
            final Specialist specialist = (Specialist) client;
            if (!specialist.getTraderMap().isEmpty()) {
                ConnectionAdaptor.logger
                        .error("Failed in asserting the emptiness of registered traders at specialist: "
                                + getClientId() + " at day " + event.getDay() + " !");
            }
        }

        setExpectedReactiveSessions(new CatpReactiveSession[] { new GetTraderSession(), new GetSpecialistSession(),
                new GetFeeSession(), new GetProfitSession(), new OracleSession("DayOpening") });

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.OPTIONS, new String[] { CatpMessage.TYPE,
                CatpMessage.DAYOPENING, CatpMessage.TIME, CatpMessage.concatenate(event.getTime()) });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new DayOpeningSession(request);
        startProactiveSession(session);
    }

    protected void processDayOpenedEvent(final DayOpenedEvent event) {

        // announce valid price lists from specialists
        final Specialist specialists[] = registry.getActiveSpecialists();
        FeesAnnouncedEvent faEvent = null;
        for (final Specialist specialist : specialists) {
            faEvent = new FeesAnnouncedEvent(specialist);
            faEvent.setTime(event.getTime());
            processFeesAnnounced(faEvent);
        }

        if (isTrader()) {
            if (((Trader) client).isSeller()) {
                setExpectedReactiveSessions(
                        new CatpReactiveSession[] { new AskFromTraderSession(), new SubscribeFromClientSession(),
                                new RegisterFromTraderSession(), new GetTraderSession(), new GetSpecialistSession(),
                                new GetFeeSession(), new GetProfitSession(), new OracleSession("DayOpened") });
            } else {
                setExpectedReactiveSessions(
                        new CatpReactiveSession[] { new BidFromTraderSession(), new SubscribeFromClientSession(),
                                new RegisterFromTraderSession(), new GetTraderSession(), new GetSpecialistSession(),
                                new GetFeeSession(), new GetProfitSession(), new OracleSession("DayOpened") });
            }
        } else {
            setExpectedReactiveSessions(new CatpReactiveSession[] { new SubscribeFromClientSession(),
                    new TransactionFromSpecialistSession(), new GetTraderSession(), new GetSpecialistSession(),
                    new GetFeeSession(), new GetProfitSession(), new OracleSession("DayOpened") });
        }

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.OPTIONS, new String[] { CatpMessage.TYPE,
                CatpMessage.DAYOPENED, CatpMessage.TIME, CatpMessage.concatenate(event.getTime()) });
        request.setTrigger(event);

        if (isTrader()) {
            // TODO: to allow multiple different private values for multiple
            // entitlements
            final double privateValue = valuer.getValue();
            request.addHeader(CatpMessage.VALUE, String.valueOf(privateValue));
        }

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new DayOpenedSession(request);
        startProactiveSession(session);
    }

    protected void processDayClosed(final DayClosedEvent event) {

        clearPendingRequestSessions();

        setExpectedReactiveSessions(new CatpReactiveSession[] { new GetTraderSession(), new GetSpecialistSession(),
                new GetFeeSession(), new GetProfitSession(), new OracleSession("DayClosed") });

        final String specialistIds[] = registry.getSpecialistIds();
        final double profits[] = new double[specialistIds.length];
        final int popularities[] = new int[specialistIds.length];
        for (int i = 0; i < profits.length; i++) {
            profits[i] = registry.getSpecialist(specialistIds[i]).getAccount().getBalance();
            popularities[i] = registry.getSpecialist(specialistIds[i]).getTraderMap().size();
        }

        final ProfitAnnouncedEvent paEvent = new ProfitAnnouncedEvent(null);
        paEvent.setTime(event.getTime());
        CatpRequest request = CatpRequest.createRequest(CatpRequest.POST,
                new String[] { CatpMessage.TYPE, CatpMessage.PROFIT, CatpMessage.ID,
                        CatpMessage.concatenate(specialistIds), CatpMessage.VALUE,
                        CatpMessage.concatenate(profits) });
        request.setTrigger(paEvent);

        request.setTag(ConnectionAdaptor.tag);

        TimableCatpProactiveSession session = new PostSession(request);
        startProactiveSession(session);

        request = CatpRequest.createRequest(CatpRequest.OPTIONS,
                new String[] { CatpMessage.TYPE, CatpMessage.DAYCLOSED, CatpMessage.ID,
                        CatpMessage.concatenate(specialistIds), CatpMessage.VALUE,
                        CatpMessage.concatenate(popularities), CatpMessage.TIME,
                        CatpMessage.concatenate(event.getTime()) });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        session = new DayClosedSession(request);
        startProactiveSession(session);
    }

    protected void processRoundOpened(final RoundOpenedEvent event) {

        ((OracleSession) reactiveSessions[reactiveSessions.length - 1]).setState("RoundOpened");

        final TimableCatpProactiveSession session = new RoundSession(CatpMessage.ROUNDOPENED, event);
        startProactiveSession(session);
    }

    protected void processRoundClosing(final RoundClosingEvent event) {

        ((OracleSession) reactiveSessions[reactiveSessions.length - 1]).setState("RoundClosing");

        final TimableCatpProactiveSession session = new RoundSession(CatpMessage.ROUNDCLOSING, event);
        startProactiveSession(session);
    }

    protected void processRoundClosed(final RoundClosedEvent event) {

        ((OracleSession) reactiveSessions[reactiveSessions.length - 1]).setState("RoundClosed");

        final TimableCatpProactiveSession session = new RoundSession(CatpMessage.ROUNDCLOSED, event);
        startProactiveSession(session);
    }

    protected void processRegistration(final RegistrationEvent event) {
        final CatpRequest request = CatpRequest.createRequest(CatpRequest.REGISTER,
                new String[] { CatpMessage.ID, event.getTraderId() });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new RegisterToSpecialistSession(request);
        startProactiveSession(session);
    }

    protected void processSubscription(final SubscriptionEvent event) {
        final CatpRequest request = CatpRequest.createRequest(CatpRequest.SUBSCRIBE,
                new String[] { CatpMessage.ID, event.getSubscriberId() });
        request.setTrigger(event);

        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new SubscribeToSpecialistSession(request);
        startProactiveSession(session);
    }

    protected void processFeesAnnounced(final FeesAnnouncedEvent event) {

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.POST,
                new String[] { CatpMessage.TYPE, CatpMessage.FEE, CatpMessage.ID, event.getSpecialist().getId(),
                        CatpMessage.VALUE, CatpMessage.concatenate(event.getSpecialist().getFees()) });
        event.setTime(clock.getTime());
        request.setTrigger(event);
        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new PostFeeSession(request);
        startProactiveSession(session);
    }

    /**
     * notifies a trader that its shout is placed.
     * 
     * @param event
     */
    protected void processShoutPlaced(final ShoutPlacedEvent event) {
        final Shout shout = event.getShout();

        if (!shout.getTrader().getId().equals(getClientId())) {
            ConnectionAdaptor.logger.fatal("Bug: placed shout has incorrect trader information !");
            return;
        }
        // successful shout made by myself
        // trader notified of the success of shout attempting to place

        if (Shout.TRACE) {
            ConnectionAdaptor.logger.info("AtS+*: " + shout);
        }

        final ShoutFromTraderSession session = pendingRequestSessions.get(shout.getId());

        if (session != null) {
            pendingRequestSessions.remove(shout.getId());

            final CatpResponse response = CatpResponse.createResponse(CatpMessage.OK,
                    new String[] { CatpMessage.ID, shout.getId(), CatpMessage.TIME,
                            CatpMessage.concatenate(event.getTime()), CatpMessage.TYPE, CatpMessage.SHOUT });

            response.setTag(ConnectionAdaptor.tag);
            dispatchOutgoingMessage(response, session);
        } else {
            ConnectionAdaptor.logger.error("A pending ShoutFromTraderSession should exist to process placing !");
        }
    }

    /**
     * This notifies a subscriber of a placed shout.
     * 
     * @param event
     */
    protected void processShoutPosted(final ShoutPostedEvent event) {

        if (Shout.TRACE) {
            ConnectionAdaptor.logger.info("ASp*: " + event.getShout());
        }

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.POST,
                new String[] { CatpMessage.TYPE, event.getShout().isAsk() ? CatpMessage.ASK : CatpMessage.BID,
                        CatpMessage.ID,
                        CatpMessage.concatenate(new String[] { event.getShout().getId(),
                                event.getShout().getTrader().getId(), event.getShout().getSpecialist().getId() }),
                        CatpMessage.VALUE, String.valueOf(event.getShout().getPrice()), CatpMessage.TIME,
                        CatpMessage.concatenate(event.getTime()) });
        request.setTrigger(event);

        request.setTag(event.getDay());

        final TimableCatpProactiveSession session = new PostSession(request);
        startProactiveSession(session);
    }

    /**
     * NOTE: this method is used to let a specialist know a shout is received
     * towards it.
     */
    protected void processShoutReceived(final ShoutReceivedEvent event) {

        if (isSpecialist()) {
            // notify specialist of this shout received and to be accepted or rejected

            if (Shout.TRACE) {
                ConnectionAdaptor.logger.info("AmSr*: " + event.getShout());
            }

            final CatpRequest request = CatpRequest.createRequest(
                    event.getShout().isAsk() ? CatpMessage.ASK : CatpMessage.BID,
                    new String[] { CatpMessage.ID, event.getShout().getId(), CatpMessage.VALUE,
                            String.valueOf(event.getShout().getPrice()), CatpMessage.TIME,
                            CatpMessage.concatenate(event.getTime()) });
            request.setTrigger(event);

            request.setTag(ConnectionAdaptor.tag);

            final TimableCatpProactiveSession session = new ShoutForwardSession(request, event.getShout().getId());
            startProactiveSession(session);
        }
    }

    protected void processShoutRejected(final ShoutRejectedEvent event) {
        // notify the trader of shout rejected

        final Shout shout = event.getShout();

        if (shout.getState() != Shout.REJECTED) {
            ConnectionAdaptor.logger.fatal("A shout rejected should be in state REJECTED !", new Exception());
            ConnectionAdaptor.logger.fatal("shout: " + shout);
            return;
        }

        if (Shout.TRACE) {
            ConnectionAdaptor.logger.info("AtSx*: " + shout);
        }

        final ShoutFromTraderSession session = pendingRequestSessions.get(shout.getId());

        if (session != null) {
            pendingRequestSessions.remove(shout.getId());

            final CatpResponse response = CatpResponse.createResponse(CatpMessage.INVALID,
                    new String[] { CatpMessage.TYPE, CatpMessage.SPECIALIST, CatpMessage.TIME,
                            CatpMessage.concatenate(event.getTime()) });
            response.setTag(ConnectionAdaptor.tag);
            dispatchOutgoingMessage(response, session);
        } else {
            ConnectionAdaptor.logger.fatal("A pending ShoutFromTraderSession should exist to process rejection !");
        }

    }

    /**
     * notifieds traders that their shouts are matched in the transaction.
     * 
     * @param event
     */
    protected void processTransactionExecuted(final TransactionExecutedEvent event) {
        // 

        // TODO: the information included in the message may be less, e.g., removing
        // the specialist info
        final CatpRequest request = CatpRequest.createRequest(CatpMessage.TRANSACTION,
                new String[] { CatpMessage.ID, CatpMessage.concatenate(new String[] {
                        event.getTransaction().getId(), event.getTransaction().getAsk().getId(),
                        event.getTransaction().getBid().getId(), event.getTransaction().getSpecialist().getId() }),
                        CatpMessage.VALUE,
                        CatpMessage.concatenate(new double[] { event.getTransaction().getPrice(),
                                event.getTransaction().getAsk().getPrice(),
                                event.getTransaction().getBid().getPrice() }),
                        CatpMessage.TIME, CatpMessage.concatenate(event.getTime()) });
        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new TransactionToTraderSession(request);
        startProactiveSession(session);
    }

    /**
     * notifies subscribers of transactions.
     * 
     * @param event
     */
    protected void processTransactionPosted(final TransactionPostedEvent event) {
        // 

        final CatpRequest request = CatpRequest.createRequest(CatpMessage.POST, new String[] { CatpMessage.TYPE,
                CatpMessage.TRANSACTION, CatpMessage.ID,
                CatpMessage.concatenate(new String[] {
                        event.getTransaction().getId(), event.getTransaction().getAsk().getId(),
                        event.getTransaction().getBid().getId(), event.getTransaction().getSpecialist().getId() }),
                CatpMessage.VALUE,
                CatpMessage.concatenate(new double[] { event.getTransaction().getPrice(),
                        event.getTransaction().getAsk().getPrice(), event.getTransaction().getBid().getPrice() }),
                CatpMessage.TIME, CatpMessage.concatenate(event.getTime()) });
        request.setTag(ConnectionAdaptor.tag);

        final TimableCatpProactiveSession session = new PostSession(request);
        startProactiveSession(session);
    }

    protected void processSimulationStarted(final SimulationStartedEvent event) {
        // do nothing
    }

    protected void processSimulationOver(final SimulationOverEvent event) {
        terminate();
        setState(ClientState.CONN_CLOSED, ClientState.getCodeDesc(ClientState.CONN_CLOSED));
    }

    private void terminate() {
        try {
            clearPendingProactiveSessions();
            clearPendingRequestSessions();
            connection.close();
        } catch (final ConnectionException e) {
            e.printStackTrace();
            ConnectionAdaptor.logger.fatal("Failed to close the connection !", e);
        }
    }

    /**
     * receives notification of control messages from GameView, or messages from
     * somewhere else, i.e. other adaptors.
     * 
     */

    public synchronized void eventOccurred(final AuctionEvent event) {

        // debug("eventOccurred <" + getClientId());

        if ((state == ClientState.FATAL) || (state == ClientState.CONN_CLOSED)) {
            ConnectionAdaptor.logger.info("Connection adaptor for " + getClientId() + " is down, so disregard "
                    + event.getClass().getSimpleName() + " !");
            // debug("eventOccurred >1" + getClientId());
            return;
        }

        // always notify valuer first so that valuer gets updated first
        if (valuer != null) {
            valuer.eventOccurred(event);
        }

        if (event instanceof GameStartingEvent) {
            processGameStarting((GameStartingEvent) event);
        } else if (event instanceof GameStartedEvent) {
            processGameStarted((GameStartedEvent) event);
        } else if (event instanceof GameOverEvent) {
            processGameOver((GameOverEvent) event);
        } else if (event instanceof DayOpeningEvent) {
            processDayOpening((DayOpeningEvent) event);
        } else if (event instanceof DayOpenedEvent) {
            processDayOpenedEvent((DayOpenedEvent) event);
        } else if (event instanceof DayClosedEvent) {
            processDayClosed((DayClosedEvent) event);
        } else if (event instanceof RoundOpenedEvent) {
            processRoundOpened((RoundOpenedEvent) event);
        } else if (event instanceof RoundClosingEvent) {
            processRoundClosing((RoundClosingEvent) event);
        } else if (event instanceof RoundClosedEvent) {
            processRoundClosed((RoundClosedEvent) event);
        } else if (event instanceof RegistrationEvent) {
            processRegistration((RegistrationEvent) event);
        } else if (event instanceof SubscriptionEvent) {
            processSubscription((SubscriptionEvent) event);
        } else if (event instanceof FeesAnnouncedEvent) {
            processFeesAnnounced((FeesAnnouncedEvent) event);
        } else if (event instanceof ShoutPlacedEvent) {
            processShoutPlaced((ShoutPlacedEvent) event);
        } else if (event instanceof ShoutReceivedEvent) {
            processShoutReceived((ShoutReceivedEvent) event);
        } else if (event instanceof ShoutRejectedEvent) {
            processShoutRejected((ShoutRejectedEvent) event);
        } else if (event instanceof ShoutPostedEvent) {
            processShoutPosted((ShoutPostedEvent) event);
        } else if (event instanceof TransactionExecutedEvent) {
            processTransactionExecuted((TransactionExecutedEvent) event);
        } else if (event instanceof TransactionPostedEvent) {
            processTransactionPosted((TransactionPostedEvent) event);
        } else if (event instanceof SimulationStartedEvent) {
            processSimulationStarted((SimulationStartedEvent) event);
        } else if (event instanceof SimulationOverEvent) {
            processSimulationOver((SimulationOverEvent) event);
        } else if (event instanceof AvailableTradersAnnouncedEvent) {
            processAvailableTradersAnnounced((AvailableTradersAnnouncedEvent) event);
        } else if (event instanceof AvailableMarketsAnnouncedEvent) {
            processAvailableMarketsAnnounced((AvailableMarketsAnnouncedEvent) event);
            // } else if (event instanceof TraderCheckInEvent) {
            // // do nothing
            // } else if (event instanceof SpecialistCheckInEvent) {
            // // do nothing
            // } else if (event instanceof ClientStateUpdatedEvent) {
            // // do nothing
        } else {
            ConnectionAdaptor.logger.fatal("Invalid event to client : " + event.getClass().getSimpleName());
            setState(ClientState.FATAL, event);
        }

        // debug("eventOccurred >2" + getClientId());
    }

    private void startProactiveSession(final TimableCatpProactiveSession session) {
        proactiveSessions.add(session);
        try {
            final TimeoutTask timeoutAction = timeController.monitor(this, session);
            session.setTimeoutAction(timeoutAction);
            session.sendRequest();
        } catch (final CatException e) {
            proactiveSessions.remove(session);
            session.forceOut();
            ConnectionAdaptor.logger.error("Failed to send request in " + session + ": \n" + session.getRequest());
        }
    }

    private void setExpectedReactiveSessions(final CatpReactiveSession sessions[]) {
        reactiveSessions = sessions;
    }

    private void dectedAndRunReactiveSessions(final CatpRequest request, final CatpReactiveSession sessions[]) {

        CatpResponse response = null;
        CatpReactiveSession session = null;

        for (final CatpReactiveSession session2 : sessions) {
            try {
                session = (CatpReactiveSession) session2.clone();
                session.processRequest(request);
                if (!session.isProcessed()) {
                    ConnectionAdaptor.logger.fatal("Bug: " + session + " in " + getClass().getSimpleName()
                            + " processed request but didn't mark _processed_:\n" + request + "\n");
                }
                return;
            } catch (final CatpMessageException e) {
                if (session.isProcessed()) {
                    final String s = "Failed in processing request from " + getClientId() + " in " + session + ".";
                    String responseType = null;
                    if (e instanceof CatpMessageInvalidException) {
                        responseType = CatpMessage.INVALID;
                    } else if (e instanceof CatpMessageErrorException) {
                        responseType = CatpMessage.ERROR;
                    }

                    if (responseType != null) {
                        response = CatpResponse.createResponse(responseType,
                                new String[] { CatpMessage.TEXT, s + " Error:" + e.toString() });

                        // use tag only when Tag is present in request.
                        if (request.getTag() != null) {
                            response.setTag(request.getTag());
                        }

                        // only send response if connection is still alive
                        // TODO: to check if the checking is sufficient.
                        if (!connection.isClosed()) {
                            dispatchOutgoingMessage(response, session);
                        }
                    }

                    ConnectionAdaptor.logger.error(s + " Request:\n" + request, e);

                    return;
                } else {
                    // do nothing

                    // continue to try to process the request with the next
                    // possible reactive session.
                }
            } catch (final CatException e) {
                ConnectionAdaptor.logger.fatal(e);
            } catch (final RuntimeException e) {
                e.printStackTrace();
                ConnectionAdaptor.logger.fatal(e);
            }
        }

        setState(ClientState.FATAL,
                ClientState.getCodeDesc(ClientState.FATAL) + " in locating proper reactive session !");
    }

    public void messageArrived(final CatpMessage msg) {
        dispatchIncomingMessage(msg);
    }

    public synchronized void handleMessage(final CatpMessage msg) {
        // debug("messageReceived <" + getClientId());

        CatpResponse response = null;
        CatpRequest request = null;

        if (msg == null) {
            // message is null, indicating connection closed
            if (state != ClientState.FATAL) {
                setState(ClientState.FATAL, ClientState.getCodeDesc(ClientState.FATAL) + " in message from client");
            }
        } else {
            if (state != ClientState.FATAL) {
                if ((ConnectionAdaptor.tag != null) && (ConnectionAdaptor.tag.length() != 0)
                        && !ConnectionAdaptor.tag.equals(msg.getTag()) && (client != null)) {
                    // simply disregard the message, but don't check when the connection
                    // is just opened
                    ConnectionAdaptor.logger.info("Message with wrong tag received from " + client.getId()
                            + " (right tag = " + ConnectionAdaptor.tag + ") :\n" + msg);

                    // debug("messageReceived >1" + getClientId());
                    return;
                }
            }

            if (msg instanceof CatpResponse) {
                response = (CatpResponse) msg;

                if (proactiveSessions.isEmpty()) {
                    if (state != ClientState.FATAL) {
                        ConnectionAdaptor.logger
                                .fatal("Unexpected response received from " + client.getId() + ":\n" + response);
                        setState(ClientState.ERROR,
                                ClientState.getCodeDesc(ClientState.ERROR) + " with unexpected message");
                        // debug("messageReceived >2" + getClientId());
                        return;
                    }
                } else {
                    final TimableCatpProactiveSession session = (TimableCatpProactiveSession) proactiveSessions
                            .remove();
                    try {
                        session.processResponse(response);
                    } catch (final CatpMessageException e) {
                        if (state != ClientState.FATAL) {
                            ConnectionAdaptor.logger.error("Failed in " + session.getClass().getSimpleName()
                                    + " with the response from " + client.getId() + " : \n" + response, e);
                            // state has been set properly, so no need to do so here
                        }
                    } catch (final CatException e) {
                        if (state != ClientState.FATAL) {
                            ConnectionAdaptor.logger.fatal(e);
                            setState(ClientState.FATAL,
                                    ClientState.getCodeDesc(ClientState.FATAL) + " in on-going " + session);
                        }
                    }
                }
            } else {
                request = (CatpRequest) msg;

                dectedAndRunReactiveSessions(request, reactiveSessions);
            }
        }

        // debug("messageReceived >3" + getClientId());

    }

    protected void dispatchEvent(final AuctionEvent event, final Collection<String> receiverIdColl) {
        manager.dispatchEvent(event, receiverIdColl, this);
    }

    protected void dispatchOutgoingMessage(CatpMessage msg, Session<CatpMessage> session) {
        // send outgoing messagen directly, instead of dispatching at the connection
        // manager
        // 
        try {
            session.sendMessage(msg);
        } catch (final CatException e) {
            ConnectionAdaptor.logger.error("Exception occurred during sending message to " + getClientId());
            ConnectionAdaptor.logger.error(msg);
            e.printStackTrace();
        }
        // manager.dispatchOutgoingMessage(message, session, getClientId(), this);
    }

    protected void dispatchIncomingMessage(CatpMessage msg) {
        manager.dispatchIncomingMessage(msg, this, getClientId(), this);
    }

    /**
     * processes timeout event from TimeController on sessions.
     * 
     */
    public synchronized void timeout(final TimableCatpProactiveSession session) {

        // debug("timeout <" + getClientId());

        // TODO: whether or not to make the situation worse for the following
        // exceptions?

        if (session.isCompleted()) {
            /*
             * There are at least two cases that a timeout session turns out to have
             * completed:
             * 
             * Case 1: A session receives a response after it times out but before
             * this timeout() method is invoked.
             * 
             * Case 2: A session times out but got cleared out of pending proactive
             * sessions due to a previous timeout error before this timeout() method
             * is invoked.
             * 
             * On both cases, isCompleted() is true.
             */
            ConnectionAdaptor.logger.warn(
                    session + " times out, but managed to either" + " receive the response in the last minute or"
                            + " get forced out in processing an earlier error !");
        } else {
            if (!proactiveSessions.contains(session)) {
                ConnectionAdaptor.logger.error(session + " with " + getClientId()
                        + " timed out, but not found in pending proactive session list !");

            } else {
                final TimableCatpProactiveSession pendingSession = (TimableCatpProactiveSession) proactiveSessions
                        .get();

                /*
                 * NOTE that it's possible that sessions scheduled later may timeout
                 * before those scheduled earlier. When this happens, the timeout
                 * session exists in proactiveSessions but not the one at the head.
                 */

                if (session == pendingSession) {
                    ConnectionAdaptor.logger
                            .error("Timeout during " + session + " with client " + client.getId() + " !");
                } else {
                    ConnectionAdaptor.logger.error("Timeout during " + session + " with client " + client.getId()
                            + " and the timeout occurred before sessions scheduled earlier timeout !\n");
                }

                proactiveSessions.remove(session);
                session.forceOut();
            }
        }

        // debug("timeout >" + getClientId());

    }

    private void setState(final int state, final String desc) {
        // no triggering event info
        setState(state, null, desc);
    }

    private void setState(final int newState, final AuctionEvent triggeringEvent) {
        // no description info
        setState(newState, triggeringEvent, null);
    }

    private void setState(final int newState, final AuctionEvent triggeringEvent, final String desc) {

        // disregard any state update after this adaptor is down.
        if (state == ClientState.CONN_CLOSED) {
            return;
        }

        final int oldState = state;
        final int nextState = calculateState(newState);

        if ((nextState == ClientState.ERROR) || (nextState == ClientState.FATAL)) {
            failed = true;
        } else if (nextState == ClientState.OK) {
            failed = false;
        }

        // if new error or recovered from error, display the change of state.
        final String msg = "State of connection to " + getClientId() + ": " + ClientState.getCodeDesc(oldState)
                + " -> " + ClientState.getCodeDesc(nextState);
        if ((newState == ClientState.FATAL) || (newState == ClientState.ERROR)) {
            if (newState == ClientState.FATAL) {
                ConnectionAdaptor.logger.fatal(msg);
            } else if (newState == ClientState.ERROR) {
                ConnectionAdaptor.logger.error(msg);
            }

            new Exception(ClientState.getCodeDesc(newState) + ": " + desc).printStackTrace();
        } else if ((newState == ClientState.OK) && (oldState == ClientState.ERROR)) {
            ConnectionAdaptor.logger.info(msg);
        }

        synchronized (CatpMessage.ID) {
            // as in CheckInSession, client is created first before notfiying manager
            // to update adaptors, without synchronization, the event may be fired to
            // manager while the adaptor is not identified.
            if (client != null) {
                final ClientStateUpdatedEvent event = new ClientStateUpdatedEvent(client, new ClientState(oldState),
                        new ClientState(nextState, desc), triggeringEvent);
                event.setTime(clock.getTime());

                state = nextState;

                controller.processEventInsideServer(event);
            } else {
                state = nextState;
            }
        }

        // do cleaning up only at the first time of changing state to FATAL or
        // CONN_CLOSED
        if ((newState == ClientState.FATAL) && (oldState != ClientState.FATAL)) {
            setExpectedReactiveSessions(
                    new CatpReactiveSession[] { new OracleSession(ClientState.getCodeDesc(state)) });

            // a specialist aims to clear the pending ShoutForwardSessions now instead
            // of at the end of the day so as for the trader clients to be notified
            // asap.
            if (isSpecialist()) {
                clearPendingProactiveSessions();
            } else {
                clearPendingRequestSessions();
            }

            // setting the state to CONN_CLOSED would cause the adaptor to be removed
            // completely
            setState(ClientState.CONN_CLOSED, triggeringEvent, "Connection closed due to fatal errors !");

        } else if (newState == ClientState.CONN_CLOSED) {
            terminate();
            setExpectedReactiveSessions(
                    new CatpReactiveSession[] { new OracleSession(ClientState.getCodeDesc(state)) });
        }
    }

    protected int calculateState(final int newState) {
        if ((state == ClientState.FATAL) && (newState != ClientState.CONN_CLOSED)) {
            // do not downgrade the level of error
            return state;
        } else if (state == ClientState.CONN_CLOSED) {
            // do not change state after connection is closed.
            return state;
        } else {
            return newState;
        }
    }

    public int getState() {
        return state;
    }

    // ////////////////////////////////////////////////////////////
    // proactive sessions
    // ////////////////////////////////////////////////////////////

    /**
     * processes Round OPENED/CLOSING/CLOSED messages that doesn't need special
     * attention to the response
     */
    class RoundSession extends TimableCatpProactiveSession {

        String type;

        public RoundSession(final String type, final AuctionEvent triggeringEvent) {
            super(connection, CatpRequest.createRequest(CatpMessage.OPTIONS, new String[] { CatpMessage.TYPE, type,
                    CatpMessage.TIME, CatpMessage.concatenate(triggeringEvent.getTime()) }));
            request.setTag(ConnectionAdaptor.tag);
            request.setTrigger(triggeringEvent);

            this.type = type;
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, type));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received from " + getClientId() + " !");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "RoundSession " + request.getHeader(CatpMessage.TYPE);
        }

    }

    class GameStartingSession extends TimableCatpProactiveSession {

        public GameStartingSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.GAMESTARTING));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());

                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to options gamestarting message!");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "GameStartingSession";
        }
    }

    class GameStartedSession extends TimableCatpProactiveSession {

        public GameStartedSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.GAMESTARTED));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to options gamestarted message!");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "GameStartedSession";
        }
    }

    class GameOverSession extends TimableCatpProactiveSession {

        public GameOverSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.GAMEOVER));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
                setState(ClientState.READY, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to options gameover message!");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "GameOverSession";
        }
    }

    /**
     * notifies specialists of day opened.
     * 
     */
    class DayOpeningSession extends TimableCatpProactiveSession {

        public DayOpeningSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.DAYOPENING));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {

                if (isSpecialist()) {
                    // for specialists, check for price list
                    if (CatpMessage.FEE.equalsIgnoreCase(response.getHeader(CatpMessage.TYPE))) {
                        final String priceList = response.getHeader(CatpMessage.VALUE);
                        if ((priceList == null) || (priceList.length() == 0)) {
                            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.FEE));
                            throw new CatpMessageErrorException("Empty price list !");
                        }

                        final double fees[] = CatpMessage.parseDoubles(priceList);
                        try {
                            chargeValidator.check(getClientId(), fees);
                        } catch (final InvalidChargeException e) {
                            // e.printStackTrace();
                            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.FEE));
                            throw new CatpMessageInvalidException(e.getMessage());
                        }

                        // ban the specialist for the current game day if there is a pending
                        // day banning penalty
                        // just ban the FeesAnnouncedEvent
                        final ClientBehaviorController behaviorController = controller.getBehaviorController();
                        if (behaviorController.getPenalty(getClientId(),
                                ClientBehaviorController.DAY_BANNING_PENALTY) > 0) {
                            behaviorController.penaltyExecuted(getClientId(),
                                    ClientBehaviorController.DAY_BANNING_PENALTY);
                            ConnectionAdaptor.logger.info("Penalty "
                                    + behaviorController.getPenalty(getClientId(),
                                            ClientBehaviorController.DAY_BANNING_PENALTY)
                                    + " pending on " + getClientId() + ".\n");
                            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.FEE));
                        } else {

                            final Specialist specialist = (Specialist) client;
                            specialist.setFees(fees);
                            final FeesAnnouncedEvent event = new FeesAnnouncedEvent(specialist);
                            event.setTime(clock.getTime());
                            controller.processEventInsideServer(event);
                            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.FEE));
                        }
                    } else {
                        setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                        throw new CatpMessageErrorException(
                                "Header FEE expected instead of " + response.getHeader(CatpMessage.TYPE)
                                        + " in the response to options dayopening request !");
                    }
                } else {
                    // for trader, check number of entitlements
                    final int entitlement = response.getIntHeader(CatpMessage.VALUE);
                    if (entitlement < 0) {
                        ConnectionAdaptor.logger.warn("Illegal negative initial entitlement of traders !");
                    }

                    final Trader trader = (Trader) client;
                    trader.setEntitlement(entitlement);
                }
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.FATAL, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to options dayopening request !");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "DayOpeningSession";
        }
    }

    class DayOpenedSession extends TimableCatpProactiveSession {

        public DayOpenedSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.DAYOPENED));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                if (isTrader()) {
                    final double privateValue = request.getDoubleHeader(CatpMessage.VALUE);

                    // TODO: did not consider private values for multiple items
                    // this may distort the supply and demand curves if not all traders
                    // have the same entitlement every day.
                    final PrivateValueAssignedEvent event = new PrivateValueAssignedEvent(getClientId(),
                            privateValue);
                    event.setTime(clock.getTime());
                    controller.processEventInsideServer(event);
                }

                setState(ClientState.OK, (AuctionEvent) request.getTrigger());

            } else {
                setState(ClientState.FATAL, (AuctionEvent) request.getTrigger());

                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to options dayopened request !");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "DayOpenedSession";
        }
    }

    class DayClosedSession extends TimableCatpProactiveSession {

        public DayClosedSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.DAYCLOSED));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.FATAL, (AuctionEvent) request.getTrigger());

                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to options dayclosed message!");
            }
        }

        public void timeout() {
            setState(ClientState.FATAL, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "DayClosedSession";
        }
    }

    class PostSession extends TimableCatpProactiveSession {

        public PostSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                final String msg = "Unexpected response received to post " + request.getHeader(CatpMessage.TYPE)
                        + " message:\n" + response;
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger(), msg);

                if (response.getStatusCode().equalsIgnoreCase(CatpResponse.INVALID)) {
                    if (getState() != ClientState.CONN_CLOSED) {
                        final String type = response.getHeader(CatpMessage.TYPE);
                        if ((type != null) && type.equalsIgnoreCase(CatpMessage.WRONGTIME)) {
                            /*
                             * the request arrived too late and was invalid for client to
                             * process, so just disregard the response.
                             */
                        } else {
                            // There may be a bug in program !

                            // TODO: when a specialist times out in responding to POST Shout
                            // messages, this may happen. Check this out!
                            ConnectionAdaptor.logger.info("Bug: unexpected scenario in " + toString());
                        }
                    }
                } else {
                    throw new CatpMessageErrorException(msg);
                }
            }
        }

        public void timeout() {
            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "PostSession " + request.getHeader(CatpMessage.TYPE);
        }
    }

    class PostTraderSession extends PostSession {
        public PostTraderSession(final CatpRequest request) {
            super(request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            // notifies game clock
            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.CLIENT));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)
                    || response.getStatusCode().equalsIgnoreCase(CatpResponse.INVALID)) {
                setState(response.getStatusCode().equalsIgnoreCase(CatpResponse.OK) ? ClientState.OK
                        : ClientState.ERROR, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException(
                        "Unexpected " + response.getStatusCode() + " response received to post trader message!");
            }

        }
    }

    class PostSpecialistSession extends PostSession {
        public PostSpecialistSession(final CatpRequest request) {
            super(request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            // notifies game clock
            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.CLIENT));

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else if (response.getStatusCode().equalsIgnoreCase(CatpResponse.INVALID)) {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received to post specialist message!");
            }
        }
    }

    class PostFeeSession extends PostSession {
        public PostFeeSession(final CatpRequest request) {
            super(request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            // notifies game clock
            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.FEE));
        }

        public String toString() {
            return super.toString() + "[" + request.getHeader(CatpMessage.ID) + "]";
        }
    }

    class RegisterToSpecialistSession extends TimableCatpProactiveSession {

        public RegisterToSpecialistSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            // TODO: to allow specialist to reject registration request using pending
            // session
        }

        public void timeout() {
            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "RegisterToSpecialistSession";
        }
    }

    class SubscribeToSpecialistSession extends TimableCatpProactiveSession {

        public SubscribeToSpecialistSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            // TODO: to allow specialist to reject subscription request using pending
            // sessions

        }

        public void timeout() {
            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "SubscribeToSpecialistSession";
        }
    }

    class ShoutForwardSession extends TimableCatpProactiveSession {

        String shoutId;

        public ShoutForwardSession(final CatpRequest request, final String shoutId) {
            super(connection, request);
            this.shoutId = shoutId;
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            if (isTrader()) {
                throw new CatpMessageErrorException("Only specialists can possibly respond to shouts!");
            }

            final Specialist specialist = (Specialist) client;
            boolean failed = false;

            synchronized (specialist) { // shout to market

                Shout currentShout = null;
                Shout newShout = registry.getShout(shoutId);

                if (newShout == null) {
                    ConnectionAdaptor.logger.fatal("Forwarded a non-existing shout to specialist !");
                    failed = true;
                } else {
                    if (newShout.getState() == Shout.PENDING) {
                        // a brand new one, do nothing

                        // logger.info("Forwarded new shout: " + shout);

                    } else if ((newShout.getState() == Shout.PLACED) || (newShout.getState() == Shout.MATCHED)) {
                        // this is a shout modification request

                        // NOTE: if shout.getState()==Shout.MATCHED, the specialist's
                        // response must be INVALID, rejecting the modification.

                        // logger.info("Forwarded modified shout: " + shout);

                        if (newShout.getChild() == null) {
                            ConnectionAdaptor.logger.fatal("Bug: either current shout shouldn't have been placed"
                                    + " or its child shouldn't be null !");
                            failed = true;
                        } else {
                            currentShout = newShout;
                            newShout = newShout.getChild();
                            if (newShout.getState() != Shout.PENDING) {
                                ConnectionAdaptor.logger
                                        .fatal("Bug: shout modifying the current one should be PENDING !");
                                failed = true;
                            }

                            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)
                                    && (currentShout.getState() != Shout.PLACED)) {
                                ConnectionAdaptor.logger.fatal(
                                        "Bug: succeeded in modifying a shout that is not in state PLACED !",
                                        new Exception("Probable bug or malicious action in specialist "
                                                + getClientId() + " !"));
                                ConnectionAdaptor.logger.fatal("current shout: " + currentShout);
                                ConnectionAdaptor.logger.fatal("modification shout: " + newShout);
                                ConnectionAdaptor.logger.fatal("\n");
                                failed = true;
                            }
                        }
                    } else {
                        ConnectionAdaptor.logger.fatal(this + ": Invalid shout state " + newShout.getState());
                        failed = true;
                    }
                    newShout.setSpecialist(specialist);
                }

                if (!failed && response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                    if (Shout.TRACE) {
                        if (currentShout != null) {
                            // shout modification
                            ConnectionAdaptor.logger.info("AS-*: " + currentShout);
                            ConnectionAdaptor.logger.info("AS+m: " + newShout);
                        } else {
                            // brand new shout
                            ConnectionAdaptor.logger.info("AS+n: " + newShout);
                        }
                    }
                    AuctionEvent event = new ShoutPlacedEvent(newShout);
                    event.setTime(clock.getTime());

                    // sends to registry, console, and reports
                    // update the current shout
                    controller.processEventInsideServer(event);

                    // forward to the trader
                    final HashSet<String> receiverIds = new HashSet<String>();
                    receiverIds.add(newShout.getTrader().getId());
                    dispatchEvent(event, receiverIds);

                    // forward to all subscribers
                    event = new ShoutPostedEvent(newShout);
                    event.setTime(clock.getTime());

                    receiverIds.clear();
                    final String ids[] = registry.getSubscriberIds(client.getId());
                    if (ids != null) {
                        for (final String id : ids) {
                            receiverIds.add(id);
                        }
                    }
                    dispatchEvent(event, receiverIds);

                    // manager.printPendingTasks();
                    // logger.info("\n");

                } else {
                    // shout rejected

                    if (Shout.TRACE) {
                        if (currentShout == null) {
                            ConnectionAdaptor.logger.info("ASxn: " + newShout);
                        } else {
                            ConnectionAdaptor.logger.info("ASxm: " + newShout);
                        }
                    }

                    final AuctionEvent event = new ShoutRejectedEvent(newShout);

                    event.setTime(clock.getTime());

                    // tell registry
                    controller.processEventInsideServer(event);

                    // forward to the trader only
                    final HashSet<String> receiverIds = new HashSet<String>();
                    receiverIds.add(newShout.getTrader().getId());
                    dispatchEvent(event, receiverIds);

                    if (!response.getStatusCode().equalsIgnoreCase(CatpResponse.INVALID)) {
                        throw new CatpMessageErrorException(
                                "Unexpected " + response.getStatusCode() + " response received !");
                    }
                }
            } // synchronization on specialist
        }

        public void timeout() {
            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger(), "Timeout in " + this);

            /*
             * create a fake response to terminate this session nicely, so that the
             * server will eventually notify the trader of the rejection of its shout.
             */
            final CatpResponse response = CatpResponse.createResponse(CatpMessage.INVALID);
            try {
                processResponse(response);
            } catch (final CatException e) {
                ConnectionAdaptor.logger.error("Failed in taking a short cut for timeout during " + this + " !", e);
            }
        }

        public String toString() {
            return "ShoutForwardSession[" + shoutId + "]";
        }
    }

    class TransactionToTraderSession extends TimableCatpProactiveSession {

        public TransactionToTraderSession(final CatpRequest request) {
            super(connection, request);
        }

        public void processResponse(final CatpResponse response) throws CatException {
            super.processResponse(response);

            if (response.getStatusCode().equalsIgnoreCase(CatpResponse.OK)) {
                setState(ClientState.OK, (AuctionEvent) request.getTrigger());
            } else {
                setState(ClientState.ERROR, (AuctionEvent) request.getTrigger());

                throw new CatpMessageErrorException("Unexpected " + response.getStatusCode()
                        + " response received from trader to transaction message!");
            }
        }

        public void timeout() {
            setState(ClientState.ERROR, (AuctionEvent) request.getTrigger(), "Timeout in " + this);
        }

        public String toString() {
            return "TransactionToTraderSession";
        }
    }

    // ////////////////////////////////////////////////////////////
    // reactive sessions
    // ////////////////////////////////////////////////////////////

    class CheckInSession extends CatpReactiveSession {

        public CheckInSession() {
            super(connection, CatpMessage.CHECKIN);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            String type = request.getHeader(CatpMessage.TYPE);
            if ((type == null) || (type.length() == 0)) {
                setState(ClientState.FATAL,
                        ClientState.getCodeDesc(ClientState.FATAL) + " with empty client type during CHECKIN");
                throw new CatpMessageErrorException("No type of cat client specified in CHECKIN message !");
            } else {
                if (IdentityOffice.isValidClientType(type)) {

                    final String desc = request.getHeader(CatpMessage.TEXT);
                    final boolean isTrader = !type.toLowerCase().startsWith(CatpMessage.SPECIALIST.toLowerCase());

                    // check for security token
                    if (!controller.getSecurityManager().isAuthorizedClient(isTrader, type)) {
                        setState(ClientState.FATAL, ClientState.getCodeDesc(ClientState.FATAL)
                                + " without security token during CHECKIN");
                        throw new CatpMessageErrorException(
                                "Unexpected " + request.getType() + " request without security token received from "
                                        + connection.getRemoteAddressInfo() + " !");
                    } else {
                        type = controller.getSecurityManager().removeToken(isTrader, type);
                    }

                    boolean isSeller = false;
                    if (isTrader) {
                        if (type.toLowerCase().startsWith(CatpMessage.SELLER.toLowerCase())) {
                            isSeller = true;
                        } else if (type.toLowerCase().startsWith(CatpMessage.BUYER.toLowerCase())) {
                            isSeller = false;
                        } else {
                            ConnectionAdaptor.logger
                                    .error("Trader type must start with " + CatpMessage.SELLER.toLowerCase()
                                            + " or " + CatpMessage.BUYER.toLowerCase() + "!");
                            ConnectionAdaptor.logger.error("Given trader type, " + type
                                    + " cannot determine whether the trader sells or buys !");
                        }
                    }

                    synchronized (CatpMessage.ID) {
                        // avoid duplicate proposed identities passed

                        // if client suggests an id, check if it is an expected specialist;
                        // or if it is a failed client; or
                        // otherwise view it a new client and use the suggested id if the id
                        // doesn't conflict with existing client; or allocate an id based on
                        // the client's type

                        String clientId = request.getHeader(CatpMessage.ID);
                        if (clientId != null) {
                            if (registry.getExpectedSpecialist(clientId) != null) {
                                // expected specialist comes in
                                client = registry.getExpectedSpecialist(clientId);
                                client.setDescription(desc);
                            } else if (registry.getFailedClient(clientId) != null) {
                                // failed client reconnects
                                client = registry.getFailedClient(clientId);

                                final ClientBehaviorController behaviorController = controller
                                        .getBehaviorController();
                                if (behaviorController.getPenalty(clientId,
                                        ClientBehaviorController.CONNECTION_BANNING_PENALTY) > 0) {
                                    manager.removeBabyAdaptor(ConnectionAdaptor.this);

                                    // do not adjust penalty to be executed here to make the
                                    // client never able to check in.

                                    final String s = "Maximum number of reconnection allowed has been reached !";
                                    setState(ClientState.FATAL, s);
                                    throw new CatpMessageErrorException(s);
                                } else {
                                    behaviorController.observe(clientId, ClientBehaviorController.RECONNECTION);
                                }
                            } else if ((isTrader && registry.containsTrader(clientId))
                                    || (!isTrader && registry.containsSpecialist(clientId))) {
                                // new client with conflicting id proposal
                                clientId = null;
                            } else {
                                // new client with good id proposal
                            }
                        } else {
                            // new client without id
                        }

                        // if new unexpected client, only proceed when game not yet started
                        if ((client == null) && clock.isActive()) {
                            final String s = "new client not allowed after game started (CHECKIN)";
                            setState(ClientState.FATAL, s);
                            throw new CatpMessageErrorException(s);
                        }

                        // if new client and no id proposal, allocate an id
                        if ((clientId == null) || (clientId.length() == 0)) {
                            clientId = manager.getIdentityOffice().createIdentity(type);
                        }

                        final CatpResponse response = CatpResponse.createResponse(CatpMessage.OK,
                                new String[] { CatpMessage.ID, clientId });
                        dispatchOutgoingMessage(response, this);

                        // if new client, create client object
                        if (client == null) {
                            if (isTrader) {
                                client = new Trader(clientId, desc, isSeller);
                            } else {
                                client = new Specialist(clientId, desc);
                            }
                        }

                        AuctionEvent event = null;
                        if (client instanceof Trader) {
                            event = new TraderCheckInEvent((Trader) client);
                        } else {
                            event = new SpecialistCheckInEvent((Specialist) client);
                        }

                        event.setTime(clock.getTime());
                        controller.processEventInsideServer(event);

                        ConnectionAdaptor.logger.debug("Id assigned: " + clientId);

                        manager.clientCheckIn(ConnectionAdaptor.this);

                    } // end of synchronization

                } else {
                    setState(ClientState.FATAL, ClientState.getCodeDesc(ClientState.FATAL)
                            + " with invalid client type during CHECKIN");
                    throw new CatpMessageErrorException("Unexpected " + request.getType() + " request received !");
                }
            }
        }

        public String toString() {
            return "CheckInSession";
        }

    }

    class RegisterFromTraderSession extends CatpReactiveSession {

        protected Shout shout;

        public RegisterFromTraderSession() {
            super(connection, CatpMessage.REGISTER);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            if (!(isTrader())) {
                // this case should be a bug
                setState(ClientState.FATAL, (AuctionEvent) request.getTrigger());
                throw new CatpMessageErrorException(
                        reqType + " request came from a non-trader: " + getClientId() + " !");
            }

            eventEngine.dispatchEvent(GameClock.class, new Event(this, CatpMessage.REGISTER));

            String brokerId = registry.getBrokerId(client.getId());

            CatpResponse response = null;
            if (brokerId != null) {
                response = CatpResponse.createResponse(CatpMessage.INVALID,
                        new String[] { CatpMessage.TEXT, "Already registered with " + brokerId + "." });
                response.setTag(request.getTag());
                dispatchOutgoingMessage(response, this);
            } else {
                brokerId = request.getHeader(CatpMessage.ID);
                if ((brokerId == null) || (brokerId.length() == 0)) {
                    // select not to choose any specialist

                    response = CatpResponse.createResponse(CatpMessage.OK);
                    response.setHeader(CatpMessage.TEXT, CatpMessage.REGISTER);
                    response.setTag(request.getTag());
                    dispatchOutgoingMessage(response, this);

                    ConnectionAdaptor.logger.debug("no specialist selected by " + client.getId());

                } else {
                    // only active specialists allowed to be registered with
                    final Specialist specialist = registry.getActiveSpecialist(brokerId);

                    if (specialist != null) {

                        // TODO: wait until the successful response from specialist to
                        // acknowledge the success of the request.

                        response = CatpResponse.createResponse(CatpMessage.OK);
                        response.setTag(request.getTag());
                        dispatchOutgoingMessage(response, this);

                        final RegistrationEvent event = new RegistrationEvent(client.getId(), brokerId);
                        event.setTime(clock.getTime());
                        controller.processEventInsideServer(event);

                        // forward to the specialist
                        final HashSet<String> receiverIds = new HashSet<String>();
                        receiverIds.add(brokerId);
                        dispatchEvent(event, receiverIds);

                    } else {
                        throw new CatpMessageInvalidException(
                                "Invalid or dead specialist ID in register request !");
                    }
                }
            }
        }

        public String toString() {
            return "RegisterFromTraderSession";
        }

    }

    class SubscribeFromClientSession extends CatpReactiveSession {

        public SubscribeFromClientSession() {
            super(connection, CatpMessage.SUBSCRIBE);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            final String idList = request.getHeader(CatpMessage.ID);
            if ((idList == null) || (idList.length() == 0)) {
                throw new CatpMessageErrorException("Empty specialist list !");
            } else {

                final String specialistIds[] = CatpMessage.parseStrings(idList);
                boolean hasInactiveSpecialist = false;
                final String goodIds[] = new String[specialistIds.length];
                for (int i = 0; i < specialistIds.length; i++) {
                    if (registry.getActiveSpecialist(specialistIds[i]) != null) {
                        goodIds[i] = specialistIds[i];
                    } else {
                        hasInactiveSpecialist = true;
                    }
                }

                // TODO: wait until the successful response from specialist to
                // acknowledge the success of requests.

                final CatpResponse response = CatpResponse.createResponse(CatpMessage.OK,
                        new String[] { CatpMessage.TEXT, CatpMessage.SUBSCRIBE });
                if (hasInactiveSpecialist) {
                    // specify those succeeded
                    response.setHeader(CatpMessage.ID, CatpMessage.concatenate(goodIds));
                }
                response.setTag(request.getTag());
                dispatchOutgoingMessage(response, this);

                final int time[] = clock.getTime();
                final HashSet<String> receiverIds = new HashSet<String>();
                for (final String goodId : goodIds) {
                    if (goodId != null) {
                        final AuctionEvent event = new SubscriptionEvent(getClientId(), goodId);
                        event.setTime(time);
                        controller.processEventInsideServer(event);

                        receiverIds.clear();
                        receiverIds.add(goodId);
                        dispatchEvent(event, receiverIds);
                    }
                }
            }
        }

        public String toString() {
            return "SubscribeFromClientSession";
        }

    }

    class GetTraderSession extends CatpReactiveSession {
        public GetTraderSession() {
            super(connection, CatpMessage.GET, CatpMessage.TRADER);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            final CatpResponse response = CatpResponse.createResponse(CatpMessage.OK,
                    new String[] { CatpMessage.TYPE, CatpMessage.TRADER, CatpMessage.ID,
                            CatpMessage.concatenate(registry.getWorkingTraderIds()) });

            request.setTag(ConnectionAdaptor.tag);
            dispatchOutgoingMessage(response, this);
        }

        public String toString() {
            return "GetTraderSession";
        }
    }

    class GetSpecialistSession extends CatpReactiveSession {
        public GetSpecialistSession() {
            super(connection, CatpMessage.GET, CatpMessage.SPECIALIST);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            final CatpResponse response = CatpResponse.createResponse(CatpMessage.OK,
                    new String[] { CatpMessage.TYPE, CatpMessage.SPECIALIST, CatpMessage.ID,
                            CatpMessage.concatenate(registry.getWorkingSpecialistIds()) });

            request.setTag(ConnectionAdaptor.tag);
            dispatchOutgoingMessage(response, this);
        }

        public String toString() {
            return "GetSpecialistSession";
        }
    }

    class GetProfitSession extends CatpReactiveSession {
        public GetProfitSession() {
            super(connection, CatpMessage.GET, CatpMessage.PROFIT);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            final String specialistIds[] = registry.getWorkingSpecialistIds();
            final double profits[] = new double[specialistIds.length];
            for (int i = 0; i < profits.length; i++) {
                profits[i] = registry.getWorkingSpecialist(specialistIds[i]).getAccount().getBalance();
            }

            final CatpResponse response = CatpResponse.createResponse(CatpMessage.OK,
                    new String[] { CatpMessage.TYPE, CatpMessage.PROFIT, CatpMessage.ID,
                            CatpMessage.concatenate(specialistIds), CatpMessage.VALUE,
                            CatpMessage.concatenate(profits) });

            request.setTag(ConnectionAdaptor.tag);
            dispatchOutgoingMessage(response, this);
        }

        public String toString() {
            return "GetProfitSession";
        }
    }

    class GetFeeSession extends CatpReactiveSession {
        public GetFeeSession() {
            super(connection, CatpMessage.GET, CatpMessage.FEE);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            final String specialistId = request.getHeader(CatpMessage.ID);
            final Specialist specialist = registry.getActiveSpecialist(specialistId);
            CatpResponse response = null;
            if (specialist != null) {
                response = CatpResponse.createResponse(CatpMessage.OK,
                        new String[] { CatpMessage.TYPE, CatpMessage.FEE, CatpMessage.ID, specialistId,
                                CatpMessage.VALUE, CatpMessage.concatenate(specialist.getFees()) });
            } else {
                response = CatpResponse.createResponse(CatpMessage.INVALID, new String[] { CatpMessage.TYPE,
                        CatpMessage.SPECIALIST, CatpMessage.TEXT, "Specialist doesn't exist or is inactive." });
            }

            request.setTag(ConnectionAdaptor.tag);
            dispatchOutgoingMessage(response, this);
        }

        public String toString() {
            return "GetFeeSession";
        }
    }

    /**
     * processes new shout or modified shout from trader
     */
    class ShoutFromTraderSession extends CatpReactiveSession {

        protected Shout shout;

        public ShoutFromTraderSession(final String reqType) {
            super(connection, reqType);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            if (!(isTrader())) {
                throw new CatpMessageInvalidException(
                        reqType + " request came from a non-trader: " + getClientId() + " !");
            }

            final Trader trader = (Trader) client;

            final String brokerId = registry.getBrokerId(client.getId());
            if (brokerId == null) {
                throw new CatpMessageInvalidException(
                        "Trader must first register with a specialist before making shouts !");
            }

            final Specialist specialist = registry.getActiveSpecialist(brokerId);
            if (specialist == null) {
                throw new CatpMessageInvalidException("Specialist " + brokerId + " is not active!");
            }

            synchronized (specialist) { // shout from trader

                final double price = request.getDoubleHeader(CatpMessage.VALUE);

                try {
                    shoutValidator.check(trader.isSeller(), price, valuer.getValue());
                } catch (final IllegalShoutException e) {
                    e.printStackTrace();
                    throw new CatpMessageInvalidException(e.getMessage());
                } catch (final Exception e) {
                    e.printStackTrace();
                    throw new CatpMessageErrorException(e.getMessage());
                }

                CatpResponse response = null;
                String shoutId = request.getHeader(CatpMessage.ID);
                Shout currentShout = null;
                if (shoutId == null) {
                    // brand new shout

                    // TODO: there should be no other shouts in registry, check it!

                    shoutId = manager.getIdentityOffice().createIdentity(reqType);
                    shout = new Shout(shoutId, price, reqType.equalsIgnoreCase(CatpRequest.BID));
                } else {
                    // modifying shout
                    currentShout = registry.getShout(shoutId);

                    if (currentShout == null) {
                        throw new CatpMessageErrorException(
                                "Possible bug: attempting to modify a non-existing shout !");
                    } else {
                        if (currentShout.getChild() != null) {
                            throw new CatpMessageErrorException("At most one on-going shout request is expected !");
                        }

                        if (currentShout.getState() == Shout.PLACED) {
                            // not accepted yet, can modify
                            // set the modified one as child of the current one
                            shout = new Shout(shoutId, price, reqType.equalsIgnoreCase(CatpRequest.BID));
                        } else if (currentShout.getState() == Shout.MATCHED) {
                            // already accepted, reject the modification right away
                            response = CatpResponse.createResponse(CatpMessage.INVALID,
                                    new String[] { CatpMessage.TYPE, CatpMessage.SPECIALIST, CatpMessage.TEXT,
                                            "Current shout accepted already !", CatpMessage.TIME,
                                            CatpMessage.concatenate(clock.getTime()) });
                        } else {
                            throw new CatpMessageErrorException(
                                    "Possible bug: shout to be modified has invalid state !");
                        }
                    }
                }

                if (shout != null) {
                    // make this session pending, waiting for specialist's decision
                    final String key = shoutId;
                    final ShoutFromTraderSession session = pendingRequestSessions.get(key);

                    if (session != null) {
                        final String s = "Possible bug: no pending ShoutFromTraderSession expected !";
                        new Exception(s).printStackTrace();
                        throw new CatpMessageErrorException(s);
                    }

                    pendingRequestSessions.put(key, this);

                    // need send request to specialist
                    shout.setTrader((Trader) client);

                    final AuctionEvent event = new ShoutReceivedEvent(shout);
                    event.setTime(clock.getTime());

                    // sends to registry, console, and reports
                    controller.processEventInsideServer(event);

                    // forward to the broker
                    final HashSet<String> receivers = new HashSet<String>();
                    receivers.add(brokerId);
                    dispatchEvent(event, receivers);
                } else if (response == null) {
                    throw new CatpMessageErrorException(
                            "Possible bug: response should be ready since shout is null and is not going to be forwarded to specialist !");
                } else {
                    response.setTag(request.getTag());
                    dispatchOutgoingMessage(response, this);
                }
            } // synchronization on specialist
        }

        public String toString() {
            return "ShoutFromTraderSession[" + shout + "]";
        }

    }

    class BidFromTraderSession extends ShoutFromTraderSession {
        public BidFromTraderSession() {
            super(CatpMessage.BID);
        }

        public String toString() {
            return "BidFromTraderSession";
        }

    }

    class AskFromTraderSession extends ShoutFromTraderSession {
        public AskFromTraderSession() {
            super(CatpMessage.ASK);
        }

        public String toString() {
            return "AskFromTraderSession";
        }
    }

    class TransactionFromSpecialistSession extends CatpReactiveSession {
        public TransactionFromSpecialistSession() {
            super(connection, CatpMessage.TRANSACTION);
        }

        public void processRequest(final CatpRequest request) throws CatException {
            super.processRequest(request);

            if (isTrader()) {
                throw new CatpMessageErrorException(
                        reqType + " request came from a trader: " + getClientId() + " !");
            }

            final Specialist specialist = (Specialist) client;
            synchronized (specialist) { // transaction from market

                CatpResponse response = null;

                final String idList = request.getHeader(CatpMessage.ID);
                String ids[] = null;
                String s = null;
                if ((idList == null) || (idList.length() == 0)) {
                    s = "Empty id list in " + reqType + " request !";
                    ConnectionAdaptor.logger.error(s);
                    throw new CatpMessageErrorException(s);
                } else {
                    ids = CatpMessage.parseStrings(idList);
                    if (ids.length != 2) {
                        s = "Invalid id list length in " + reqType + " request !";
                        ConnectionAdaptor.logger.error(s);
                        throw new CatpMessageErrorException(s);
                    }
                }

                final String priceList = request.getHeader(CatpMessage.VALUE);
                double prices[] = null;
                if ((priceList == null) || (priceList.length() == 0)) {
                    s = "Empty price list in " + reqType + " request !";
                    ConnectionAdaptor.logger.error(s);
                    throw new CatpMessageErrorException(s);
                } else {
                    prices = CatpMessage.parseDoubles(priceList);
                    if (prices.length != 1) {
                        throw new CatpMessageErrorException(
                                "Price list in " + reqType + " request should contain 1 numeric value !");
                    }
                }

                final Shout ask = registry.getShout(ids[0]);
                final Shout bid = registry.getShout(ids[1]);

                try {
                    transactionValidator.check(ask, bid, prices[0]);
                } catch (final IllegalTransactionException e) {
                    if (e instanceof IllegalShoutInTransactionException) {
                        response = CatpResponse.createResponse(CatpMessage.INVALID, new String[] { CatpMessage.TEXT,
                                e.getMessage(), CatpMessage.TIME, CatpMessage.concatenate(clock.getTime()) });
                    } else if (e instanceof IllegalTransactionPriceException) {
                        response = CatpResponse.createResponse(CatpMessage.INVALID,
                                new String[] { CatpMessage.TEXT, e.getMessage(), CatpMessage.TIME,
                                        CatpMessage.concatenate(clock.getTime()), CatpMessage.TYPE,
                                        CatpMessage.VALUE });
                    } else {
                        // TODO: should never reach this
                        response = CatpResponse.createResponse(CatpMessage.INVALID);
                    }

                    response.setTag(request.getTag());
                    dispatchOutgoingMessage(response, this);
                    return;
                }

                if (Shout.TRACE) {
                    ConnectionAdaptor.logger.info("ATa: " + ask);
                    ConnectionAdaptor.logger.info("ATb: " + bid);
                }

                final String transactionId = manager.getIdentityOffice().createIdentity(request.getType());

                response = CatpResponse.createResponse(CatpMessage.OK, new String[] { CatpMessage.ID, transactionId,
                        CatpMessage.TIME, CatpMessage.concatenate(clock.getTime()) });

                response.setTag(request.getTag());
                dispatchOutgoingMessage(response, this);

                // the transaction price may be slightly outside [ask, bid] range, so
                // adjust it
                if (prices[0] > bid.getPrice()) {
                    prices[0] = bid.getPrice();
                } else if (prices[0] < ask.getPrice()) {
                    prices[0] = ask.getPrice();
                }

                final Transaction transaction = new Transaction(transactionId, ask, bid, prices[0]);
                transaction.setSpecialist((Specialist) client);

                AuctionEvent event = new TransactionExecutedEvent(transaction);
                event.setTime(clock.getTime());

                // event to registry, console, and reports
                controller.processEventInsideServer(event);

                // forward to traders involved in the transaction
                final Set<String> receiverIds = new LinkedHashSet<String>();
                receiverIds.add(ask.getTrader().getId());
                receiverIds.add(bid.getTrader().getId());
                dispatchEvent(event, receiverIds);

                // forward to the trader, and all subscribers
                event = new TransactionPostedEvent(transaction);
                event.setTime(clock.getTime());
                receiverIds.clear();
                ids = registry.getSubscriberIds(client.getId());
                if (ids != null) {
                    for (final String id : ids) {
                        receiverIds.add(id);
                    }
                }

                dispatchEvent(event, receiverIds);

            } // synchronization on specialist
        }

        public String toString() {
            return "TransactionFromSpecialistSession";
        }
    }

    /**
     * a session deals with all unexpected request.
     */
    class OracleSession extends CatpReactiveSession {

        String state;

        public OracleSession(final String state) {
            super(connection, null);
            this.state = state;
        }

        public String getState() {
            return state;
        }

        public void setState(final String state) {
            this.state = state;
        }

        public void processRequest(final CatpRequest request) throws CatException {

            setProcessed(true);

            switch (ConnectionAdaptor.this.getState()) {
            case ClientState.ERROR:
            case ClientState.FATAL:
                if (!connection.isClosed()) {
                    final CatpResponse response = CatpResponse.createResponse(CatpMessage.ERROR,
                            new String[] { CatpMessage.TEXT, "Game is over. No request will be processed. Bye !" });
                    response.setTag(request.getTag());
                    dispatchOutgoingMessage(response, this);
                }

                break;

            default:

                if (!connection.isClosed()) {
                    String responseType;
                    if (request.getType().equalsIgnoreCase(CatpMessage.ASK)
                            || request.getType().equalsIgnoreCase(CatpMessage.BID)
                            || request.getType().equalsIgnoreCase(CatpMessage.TRANSACTION)) {
                        responseType = CatpMessage.INVALID;
                    } else {
                        responseType = CatpMessage.ERROR;

                        if (request.getType().equalsIgnoreCase(CatpMessage.REGISTER)) {
                            ConnectionAdaptor.logger.error("Registration request comes at a wrong time " + state
                                    + " from " + getClientId() + "\n" + request + "\n");
                        }
                    }

                    final CatpResponse response = CatpResponse.createResponse(responseType,
                            new String[] { CatpMessage.TYPE, CatpMessage.WRONGTIME, CatpMessage.TEXT,
                                    "Requested at a wrong time " + state + "." });
                    response.setTag(request.getTag());
                    dispatchOutgoingMessage(response, this);

                    final ClientBehaviorController behaviorController = controller.getBehaviorController();
                    behaviorController.observe(getClientId(), ClientBehaviorController.REQUEST_AT_WRONG_TIME);

                    ConnectionAdaptor.logger
                            .error("request comes at a wrong time from " + getClient().getId() + ":\n" + request);
                    showReactiveSessions();
                }
            }
        }

        public String toString() {
            return "OracleSession";
        }

    }

    public String getClientId() {
        if (client == null) {
            return "unamed client";
        } else {
            return client.getId();
        }
    }

    public ReactiveConnection<CatpMessage> getConnection() {
        return connection;
    }

    protected void showReactiveSessions() {
        if (reactiveSessions.length > 0) {
            ConnectionAdaptor.logger.info("< connection adaptor for " + getClientId() + "\n");
        }
        for (final CatpReactiveSession reactiveSession : reactiveSessions) {
            ConnectionAdaptor.logger.info("\t allowed reactive session: " + reactiveSession.toString());
        }

        if (reactiveSessions.length > 0) {
            ConnectionAdaptor.logger.info(">\n");
        }
    }

    protected void clearPendingRequestSessions() {
        if (!pendingRequestSessions.isEmpty()) {

            ConnectionAdaptor.logger
                    .error(getClientId() + " has pending ShoutFromTraderSessions when day is closing: "
                            + pendingRequestSessions.keySet() + " !");

            // TODO: to deal with the pending ShoutFromTraderSessions

            final Iterator<ShoutFromTraderSession> itor = pendingRequestSessions.values().iterator();
            ShoutFromTraderSession session = null;
            while (itor.hasNext()) {
                session = itor.next();

                // TODO: to tell in the response that the session fails to complete!
                final CatpResponse response = CatpResponse.createResponse(CatpMessage.INVALID,
                        new String[] { CatpMessage.TYPE, CatpMessage.SPECIALIST, CatpMessage.TIME,
                                CatpMessage.concatenate(clock.getTime()) });
                response.setTag(ConnectionAdaptor.tag);
                dispatchOutgoingMessage(response, session);
            }

            pendingRequestSessions.clear();
        }
    }

    protected void clearPendingProactiveSessions() {
        final CatpProactiveSession sessions[] = proactiveSessions.toArray(new CatpProactiveSession[0]);
        if (sessions.length > 0) {
            ConnectionAdaptor.logger.info("< Pending proactive sessions at server for " + getClientId() + "\n");

            for (final CatpProactiveSession session : sessions) {
                ConnectionAdaptor.logger.info(" pending proactive session: " + session);
            }

            ConnectionAdaptor.logger.info(">\n");

            for (final CatpProactiveSession session : sessions) {
                session.forceOut();
                proactiveSessions.remove(session);
            }
        }

    }

    /**
     * to observe the results of dispatching events to other clients.
     * 
     * @param o
     * @param arg
     *          the AuctionEvent attempted to dispatch
     */
    public void update(final Observable o, final Object arg) {
        if (o instanceof EventDispatchingTaskOnServerSide) {
            // event dispatching failed

            final EventDispatchingTaskOnServerSide task = (EventDispatchingTaskOnServerSide) o;
            AuctionEvent event = task.getEvent();
            final String receiverId = (String) arg;

            if (event instanceof ShoutReceivedEvent) {
                // failed to forward shout to specialist

                final ShoutReceivedEvent srEvent = (ShoutReceivedEvent) event;

                event = new ShoutRejectedEvent(srEvent.getShout());

                event.setTime(clock.getTime());

                // tell registry
                controller.processEventInsideServer(event);

                // forward to the trader (myself) only
                final HashSet<String> receiverIds = new HashSet<String>();
                receiverIds.add(srEvent.getShout().getTrader().getId());
                dispatchEvent(event, receiverIds);

                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to forward shout "
                        + srEvent.getShout().getId() + " to " + receiverId + " !\n");

            } else if (event instanceof ShoutPlacedEvent) {
                // failed to notify trader or subscribers of shout placed
                // do nothing

                final ShoutPlacedEvent spEvent = (ShoutPlacedEvent) event;

                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to notify "
                        + receiverId + " of the placed shout " + spEvent.getShout().getId() + " !\n");

            } else if (event instanceof ShoutRejectedEvent) {
                // failed to notify trader of shout rejected
                // do nothing

                final ShoutRejectedEvent srEvent = (ShoutRejectedEvent) event;

                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to notify "
                        + receiverId + " of the rejected shout " + srEvent.getShout().getId() + " !\n");

            } else if (event instanceof TransactionPostedEvent) {
                // failed to notify of transaction made
                // do nothing

                final TransactionPostedEvent teEvent = (TransactionPostedEvent) event;

                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to notify "
                        + receiverId + " of the transaction " + teEvent.getTransaction().getId() + " !\n");

            } else if (event instanceof RegistrationEvent) {
                // failed to notify specialist of trader registration
                // do nothing

                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to notify "
                        + receiverId + " of registration !\n");

            } else if (event instanceof SubscriptionEvent) {
                // failed to notify specialist of client subscription
                // do nothing

                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to notify "
                        + receiverId + " of subscription !\n");
            } else {
                ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to notify "
                        + receiverId + " of " + event + " !\n");

            }
        } else if (o instanceof IncomingMessageDispatchingTask) {
            // TODO: check what to do with arg
            final IncomingMessageDispatchingTask task = (IncomingMessageDispatchingTask) o;
            ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to process task " + task
                    + " regarding message from " + task.getClientId() + ":\n" + task.getMessage() + " !\n");
        } else if (o instanceof OutgoingMessageDispatchingTask) {
            final OutgoingMessageDispatchingTask task = (OutgoingMessageDispatchingTask) o;
            ConnectionAdaptor.logger.warn("Adaptor for " + getClient().getId() + " failed to process task " + task
                    + " regarding message to " + task.getClientId() + ":\n" + task.getMessage() + " !\n");
        } else {
            ConnectionAdaptor.logger.error("Unexpected observable " + o + " with argument " + arg + " !\n");
        }
    }

    /**
     * for debug only.
     */
    protected boolean failed = false;

    protected void debug(final String s) {
        if (failed) {
            ConnectionAdaptor.logger.info("DEBUG: " + s);
        }
    }
}