/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2016, TeleStax Inc. and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * * This file incorporates work covered by the following copyright and * permission notice: * * JBoss, Home of Professional Open Source * Copyright 2007-2011, Red Hat, Inc. and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jdiameter.client.impl.app.rf; import static org.jdiameter.api.Avp.ACCOUNTING_REALTIME_REQUIRED; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.IDLE; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.OPEN; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.PENDING_BUFFERED; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.PENDING_CLOSE; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.PENDING_EVENT; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.PENDING_INTERIM; import static org.jdiameter.common.api.app.rf.ClientRfSessionState.PENDING_START; import java.io.Serializable; import org.jdiameter.api.Answer; import org.jdiameter.api.ApplicationId; import org.jdiameter.api.Avp; import org.jdiameter.api.EventListener; import org.jdiameter.api.InternalException; import org.jdiameter.api.Message; import org.jdiameter.api.OverloadException; import org.jdiameter.api.Request; import org.jdiameter.api.RouteException; import org.jdiameter.api.app.AppSession; import org.jdiameter.api.app.StateChangeListener; import org.jdiameter.api.app.StateEvent; import org.jdiameter.api.rf.ClientRfSession; import org.jdiameter.api.rf.ClientRfSessionListener; import org.jdiameter.api.rf.events.RfAccountingRequest; import org.jdiameter.client.api.ISessionFactory; import org.jdiameter.common.api.app.IAppSessionState; import org.jdiameter.common.api.app.rf.ClientRfSessionState; import org.jdiameter.common.api.app.rf.IClientRfActionContext; import org.jdiameter.common.impl.app.rf.AppRfSessionImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Client Accounting session implementation * * * @author <a href="mailto:baranowb@gmail.com"> Bartosz Baranowski </a> * @author <a href="mailto:brainslog@gmail.com"> Alexandre Mendonca </a> */ public class ClientRfSessionImpl extends AppRfSessionImpl implements EventListener<Request, Answer>, ClientRfSession { private static final Logger logger = LoggerFactory.getLogger(ClientRfSessionImpl.class); // Constants ---------------------------------------------------------------- public static final int DELIVER_AND_GRANT = 1; public static final int GRANT_AND_LOSE = 3; // Factories and Listeners -------------------------------------------------- protected transient IClientRfActionContext context; protected transient ClientRfSessionListener listener; protected static final String TIMER_NAME_INTERIM = "CLIENT_INTERIM"; protected IClientRfSessionData sessionData; public ClientRfSessionImpl(IClientRfSessionData sessionData, ISessionFactory sessionFactory, ClientRfSessionListener clientAccSessionListener, IClientRfActionContext iClientRfActionContext, StateChangeListener<AppSession> stateChangeListener, ApplicationId applicationId) { super(sessionFactory, sessionData); super.appId = applicationId; this.listener = clientAccSessionListener; this.context = iClientRfActionContext; this.sessionData = sessionData; super.addStateChangeNotification(stateChangeListener); } @Override public void sendAccountRequest(RfAccountingRequest accountRequest) throws InternalException, IllegalStateException, RouteException, OverloadException { try { sendAndStateLock.lock(); handleEvent(new Event(accountRequest)); try { session.send(accountRequest.getMessage(), this); // Store last destination information sessionData.setDestinationRealm(accountRequest.getMessage().getAvps().getAvp(Avp.DESTINATION_REALM).getDiameterIdentity()); Avp destHostAvp = accountRequest.getMessage().getAvps().getAvp(Avp.DESTINATION_HOST); if (destHostAvp != null) { sessionData.setDestinationHost(destHostAvp.getDiameterIdentity()); } } catch (Throwable t) { logger.debug("Failed to send ACR.", t); handleEvent(new Event(Event.Type.FAILED_SEND_RECORD, accountRequest)); } } catch (Exception exc) { throw new InternalException(exc); } finally { sendAndStateLock.unlock(); } } protected synchronized void storeToBuffer(Request accountRequest) { sessionData.setBuffer(accountRequest); } protected synchronized boolean checkBufferSpace() { return sessionData.getBuffer() == null; } @SuppressWarnings("unchecked") protected void setState(IAppSessionState newState) { IAppSessionState oldState = sessionData.getClientRfSessionState(); sessionData.setClientRfSessionState((ClientRfSessionState) newState); for (StateChangeListener i : stateListeners) { i.stateChanged(this,(Enum) oldState, (Enum) newState); } } @Override public boolean isStateless() { return false; } @Override public boolean handleEvent(StateEvent event) throws InternalException, OverloadException { final ClientRfSessionState state = this.sessionData.getClientRfSessionState(); ClientRfSessionState oldState = state; try { switch (state) { // Idle ========== case IDLE: { switch ((Event.Type) event.getType()) { // Client or device requests access case SEND_START_RECORD: // Current State: IDLE // Event: Client or Device Requests access // Action: Send accounting start req. // New State: PENDING_S setState(PENDING_START); break; case SEND_EVENT_RECORD: // Current State: IDLE // Event: Client or device requests a one-time service // Action: Send accounting event req // New State: PENDING_E setState(PENDING_EVENT); break; // Send buffered message action in other section of this method see below default: throw new IllegalStateException("Current state " + state + " action " + event.getType()); } break; } // PendingS ========== case PENDING_START: { switch ((Event.Type) event.getType()) { case FAILED_SEND_RECORD: RfAccountingRequest request = (RfAccountingRequest) event.getData(); Avp accRtReq = request.getMessage().getAvps().getAvp(ACCOUNTING_REALTIME_REQUIRED); // Current State: PENDING_S // Event: Failure to send and buffer space available and realtime not equal to DELIVER_AND_GRANT // Action: Store Start Record // New State: OPEN if (checkBufferSpace() && accRtReq != null && accRtReq.getInteger32() != DELIVER_AND_GRANT) { storeToBuffer((Request) request.getMessage()); setState(OPEN); } else { // Current State: PENDING_S // Event: Failure to send and no buffer space available and realtime equal to GRANT_AND_LOSE // Action: - // New State: OPEN if (!checkBufferSpace() && accRtReq != null && accRtReq.getInteger32() == GRANT_AND_LOSE) { setState(OPEN); } else { // Current State: PENDING_S // Event: Failure to send and no buffer space available and realtime not equal to GRANT_AND_LOSE // Action: Disconnect User/Device // New State: IDLE try { if (context != null) { Request str = createSessionTermRequest(); context.disconnectUserOrDev(this, str); session.send(str, this); } } finally { setState(IDLE); } } } break; case RECEIVED_RECORD: // Current State: PENDING_S // Event: Successful accounting start answer received // Action: - // New State: OPEN processInterimIntervalAvp(event); setState(OPEN); break; case FAILED_RECEIVE_RECORD: try { RfAccountingRequest answer = (RfAccountingRequest) event.getData(); accRtReq = answer.getMessage().getAvps().getAvp(ACCOUNTING_REALTIME_REQUIRED); // Current State: PENDING_S // Event: Failed accounting start answer received and realtime equal to GRANT_AND_LOSE // Action: - // New State: OPEN if (accRtReq != null && accRtReq.getInteger32() == GRANT_AND_LOSE) { setState(OPEN); } else { // Current State: PENDING_S // Event: Failed accounting start answer received and realtime not equal to GRANT_AND_LOSE // Action: Disconnect User/Device // New State: IDLE if (accRtReq != null && accRtReq.getInteger32() != GRANT_AND_LOSE) { try { if (context != null) { Request str = createSessionTermRequest(); context.disconnectUserOrDev(this, str); session.send(str, this); } } finally { setState(IDLE); } } } } catch (Exception e) { logger.debug("Can not process answer", e); setState(IDLE); } break; case SEND_STOP_RECORD: // Current State: PENDING_S // Event: User service terminated // Action: Store stop record // New State: PENDING_S if (context != null) { Request str = createSessionTermRequest(); context.disconnectUserOrDev(this, str); storeToBuffer(str); } break; } break; } // OPEN ========== case OPEN: { switch ((Event.Type) event.getType()) { // User service terminated case SEND_STOP_RECORD: // Current State: OPEN // Event: User service terminated // Action: Send accounting stop request // New State: PENDING_L setState(PENDING_CLOSE); break; case SEND_INTERIM_RECORD: // FIXME: Shouldn't this be different ? // Current State: OPEN // Event: Interim interval elapses // Action: Send accounting interim record // New State: PENDING_I setState(PENDING_INTERIM); break; // Create timer for "Interim interval elapses" event case RECEIVED_RECORD: processInterimIntervalAvp(event); break; } } break; //FIXME: add check for abnormal // PendingI ========== case PENDING_INTERIM: { switch ((Event.Type) event.getType()) { case RECEIVED_RECORD: // Current State: PENDING_I // Event: Successful accounting interim answer received // Action: - // New State: OPEN processInterimIntervalAvp(event); setState(OPEN); break; case FAILED_SEND_RECORD: RfAccountingRequest request = (RfAccountingRequest) event.getData(); Avp accRtReq = request.getMessage().getAvps().getAvp(ACCOUNTING_REALTIME_REQUIRED); // Current State: PENDING_I // Event: Failure to send and buffer space available (or old record interim can be overwritten) and realtime not equal to DELIVER_AND_GRANT // Action: Store interim record // New State: OPEN if (checkBufferSpace() && accRtReq != null && accRtReq.getInteger32() != DELIVER_AND_GRANT) { storeToBuffer((Request) request.getMessage()); setState(OPEN); } else { // Current State: PENDING_I // Event: Failure to send and no buffer space available and realtime equal to GRANT_AND_LOSE // Action: - // New State: OPEN if (!checkBufferSpace() && accRtReq != null && accRtReq.getInteger32() == GRANT_AND_LOSE) { setState(OPEN); } else { // Current State: PENDING_I // Event: Failure to send and no buffer space available and realtime not equal to GRANT_AND_LOSE // Action: Disconnect User/Device // New State: IDLE if (!checkBufferSpace() && accRtReq != null && accRtReq.getInteger32() != GRANT_AND_LOSE) { try { if (context != null) { Request str = createSessionTermRequest(); context.disconnectUserOrDev(this, str); session.send(str, this); } } finally { setState(IDLE); } } } } break; case FAILED_RECEIVE_RECORD: try { RfAccountingRequest answer = (RfAccountingRequest) event.getData(); accRtReq = answer.getMessage().getAvps().getAvp(ACCOUNTING_REALTIME_REQUIRED); // Current State: PENDING_I // Event: Failed accounting interim answer received and realtime equal to GRANT_AND_LOSE // Action: - // New State: OPEN if (accRtReq != null && accRtReq.getInteger32() == GRANT_AND_LOSE) { setState(OPEN); } else { // Current State: PENDING_I // Event: Failed account interim answer received and realtime not equal to GRANT_AND_LOSE // Action: Disconnect User/Device // New State: IDLE if (accRtReq != null && accRtReq.getInteger32() != GRANT_AND_LOSE) { try { if (context != null) { Request str = createSessionTermRequest(); context.disconnectUserOrDev(this, str); session.send(str, this); } } finally { setState(IDLE); } } } } catch (Exception e) { logger.debug("Can not process received request", e); setState(IDLE); } break; case SEND_STOP_RECORD: // Current State: PENDING_I // Event: User service terminated // Action: Store stop record // New State: PENDING_I if (context != null) { Request str = createSessionTermRequest(); context.disconnectUserOrDev(this, str); storeToBuffer(str); } break; } break; } // PendingE ========== case PENDING_EVENT: { switch ((Event.Type) event.getType()) { case RECEIVED_RECORD: // Current State: PENDING_E // Event: Successful accounting event answer received // Action: - // New State: IDLE setState(IDLE); break; case FAILED_SEND_RECORD: if (checkBufferSpace()) { // Current State: PENDING_E // Event: Failure to send and buffer space available // Action: Store event record // New State: IDLE RfAccountingRequest data = (RfAccountingRequest) event.getData(); storeToBuffer((Request) data.getMessage()); } // Current State: PENDING_E // Event: Failure to send and no buffer space available // Action: - // New State: IDLE setState(IDLE); break; case FAILED_RECEIVE_RECORD: // Current State: PENDING_E // Event: Failed accounting event answer received // Action: - // New State: IDLE setState(IDLE); break; } break; } // PendingB ========== case PENDING_BUFFERED: { switch ((Event.Type) event.getType()) { case RECEIVED_RECORD: // Current State: PENDING_B // Event: Successful accounting answer received // Action: Delete record // New State: IDLE synchronized (this) { storeToBuffer(null); } setState(IDLE); break; // Failure to send case FAILED_SEND_RECORD: // Current State: PENDING_B // Event: Failure to send // Action: - // New State: IDLE setState(IDLE); break; // Failed accounting answer received case FAILED_RECEIVE_RECORD: // Current State: PENDING_B // Event: Failed accounting answer received // Action: Delete record // New State: IDLE synchronized (this) { storeToBuffer(null); } setState(IDLE); break; } break; } // PendingL ========== case PENDING_CLOSE: { switch ((Event.Type) event.getType()) { case RECEIVED_RECORD: // Current State: PENDING_L // Event: Successful accounting stop answer received // Action: - // New State: IDLE setState(IDLE); break; case FAILED_SEND_RECORD: if (checkBufferSpace()) { // Current State: PENDING_L // Event: Failure to send and buffer space available // Action: Store stop record // New State: IDLE RfAccountingRequest data = (RfAccountingRequest) event.getData(); storeToBuffer((Request) data.getMessage()); } // Current State: PENDING_L // Event: Failure to send and no buffer space available // Action: - // New State: IDLE setState(IDLE); break; // Failed accounting stop answer received case FAILED_RECEIVE_RECORD: // Current State: PENDING_L // Event: Failed accounting stop answer received // Action: - // New State: IDLE setState(IDLE); break; } break; } } // Post processing if (oldState != state) { switch (state) { // IDLE =========== case IDLE: { // Current State: IDLE // Event: Records in storage // Action: Send record // New State: PENDING_B try { synchronized (this) { if (sessionData.getBuffer() != null) { session.send(sessionData.getBuffer(), this); setState(PENDING_BUFFERED); } } } catch (Exception e) { logger.debug("can not send buffered message", e); synchronized (this) { Request buffer = sessionData.getBuffer(); if (context != null && buffer != null) { if (!context.failedSendRecord(this, buffer)) { storeToBuffer(null); } } } } } } } } catch (Throwable t) { throw new InternalException(t); } return true; } protected void processInterimIntervalAvp(StateEvent event) throws InternalException { // Avp interval = ((AppEvent) event.getData()).getMessage().getAvps().getAvp(Avp.ACCT_INTERIM_INTERVAL); // if (interval != null) { // // create timer // try { // long v = interval.getUnsigned32(); // if (v != 0) { // // scheduler.schedule( // // new Runnable() { // // public void run() { // // if (context != null) { // // try { // // Request interimRecord = createInterimRecord(); // // context.interimIntervalElapses(interimRecord); // // sendAndStateLock.lock(); // // session.send(interimRecord, ClientRfSessionImpl.this); // // setState(PENDING_INTERIM); // // } // // catch (Exception e) { // // logger.debug("Can not process Interim Interval AVP", e); // // } // // finally { // // sendAndStateLock.unlock(); // // } // // } // // } // // }, // // v, TimeUnit.SECONDS // // ); // cancelInterimTimer(); // this.timerId_interim = startInterimTimer(v); // } // } // catch (AvpDataException e) { // logger.debug("Unable to retrieve Acct-Interim-Interval AVP value", e); // } // } } /* (non-Javadoc) * @see org.jdiameter.common.impl.app.AppSessionImpl#onTimer(java.lang.String) */ @Override public void onTimer(String timerName) { if (timerName.equals(IDLE_SESSION_TIMER_NAME)) { checkIdleAppSession(); } else if (timerName.equals(TIMER_NAME_INTERIM)) { if (context != null) { try { Request interimRecord = createInterimRecord(); context.interimIntervalElapses(this, interimRecord); sendAndStateLock.lock(); session.send(interimRecord, ClientRfSessionImpl.this); setState(PENDING_INTERIM); sessionData.setTsTimerId(null); } catch (Exception e) { logger.debug("Can not process Interim Interval AVP", e); } finally { sendAndStateLock.unlock(); } } } else { logger.warn("Received an unknown timer '{}' for Session-ID '{}'", timerName, getSessionId()); } } private void startInterimTimer(long v) { try { sendAndStateLock.lock(); sessionData.setTsTimerId(super.timerFacility.schedule(getSessionId(), TIMER_NAME_INTERIM, v)); return; } finally { sendAndStateLock.unlock(); } } private void cancelInterimTimer() { try { sendAndStateLock.lock(); final Serializable timerId = this.sessionData.getTsTimerId(); if (timerId != null) { super.timerFacility.cancel(timerId); this.sessionData.setTsTimerId(null); } } finally { sendAndStateLock.unlock(); } } @Override @SuppressWarnings("unchecked") public <E> E getState(Class<E> eClass) { return eClass == ClientRfSessionState.class ? (E) this.sessionData.getTsTimerId() : null; } @Override public void receivedSuccessMessage(Request request, Answer answer) { if (request.getCommandCode() == RfAccountingRequest.code) { // state should be changed before event listener call try { sendAndStateLock.lock(); handleEvent(new Event(createAccountAnswer(answer))); } catch (Exception e) { logger.debug("Can not process received request", e); } finally { sendAndStateLock.unlock(); } try { listener.doRfAccountingAnswerEvent(this, createAccountRequest(request), createAccountAnswer(answer)); } catch (Exception e) { logger.debug("Unable to deliver message to listener.", e); } } else { try { listener.doOtherEvent(this, createAccountRequest(request), createAccountAnswer(answer)); } catch (Exception e) { logger.debug("Can not process received request", e); } } } @Override public void timeoutExpired(Request request) { try { sendAndStateLock.lock(); handleEvent(new Event(Event.Type.FAILED_RECEIVE_RECORD, createAccountRequest(request))); } catch (Exception e) { logger.debug("Can not handle timeout event", e); } finally { sendAndStateLock.unlock(); } } @Override public Answer processRequest(Request request) { if (request.getCommandCode() == RfAccountingRequest.code) { try { // FIXME Is this wrong? listener.doRfAccountingAnswerEvent(this, createAccountRequest(request), null); } catch (Exception e) { logger.debug("Can not process received request", e); } } else { try { listener.doOtherEvent(this, createAccountRequest(request), null); } catch (Exception e) { logger.debug("Can not process received request", e); } } return null; } /* (non-Javadoc) * @see org.jdiameter.common.impl.app.AppSessionImpl#isReplicable() */ @Override public boolean isReplicable() { return true; } protected Request createInterimRecord() { Request interimRecord = session.createRequest(RfAccountingRequest.code, appId, sessionData.getDestinationRealm(), sessionData.getDestinationHost()); interimRecord.getAvps().addAvp(Avp.ACC_RECORD_TYPE, 3); return interimRecord; } protected Request createSessionTermRequest() { return session.createRequest(Message.SESSION_TERMINATION_REQUEST, appId, sessionData.getDestinationRealm(), sessionData.getDestinationHost()); } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((sessionData == null) ? 0 : sessionData.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!super.equals(obj)) { return false; } if (getClass() != obj.getClass()) { return false; } ClientRfSessionImpl other = (ClientRfSessionImpl) obj; if (sessionData == null) { if (other.sessionData != null) { return false; } } else if (!sessionData.equals(other.sessionData)) { return false; } return true; } @Override public void release() { if (isValid()) { try { sendAndStateLock.lock(); //TODO: cancel timer? super.release(); } catch (Exception e) { logger.debug("Failed to release session", e); } finally { sendAndStateLock.unlock(); } } else { logger.debug("Trying to release an already invalid session, with Session ID '{}'", getSessionId()); } } }