/** * ` * Copyright 2010 Voxeo Corporation Licensed under the Apache License, * Version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law * or agreed to in writing, software distributed under the License is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package com.voxeo.moho.voicexml; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.media.mscontrol.MediaEventListener; import javax.media.mscontrol.MediaObject; import javax.media.mscontrol.MediaSession; import javax.media.mscontrol.MsControlException; import javax.media.mscontrol.Parameter; import javax.media.mscontrol.Parameters; import javax.media.mscontrol.join.Joinable; import javax.media.mscontrol.join.Joinable.Direction; import javax.media.mscontrol.vxml.VxmlDialog; import javax.media.mscontrol.vxml.VxmlDialogEvent; import org.apache.log4j.Logger; import com.voxeo.moho.ApplicationContextImpl; import com.voxeo.moho.Call; import com.voxeo.moho.Endpoint; import com.voxeo.moho.JoinWorker; import com.voxeo.moho.JoineeData; import com.voxeo.moho.Joint; import com.voxeo.moho.JointImpl; import com.voxeo.moho.MediaException; import com.voxeo.moho.Participant; import com.voxeo.moho.ParticipantContainer; import com.voxeo.moho.Unjoint; import com.voxeo.moho.UnjointImpl; import com.voxeo.moho.common.event.DispatchableEventSource; import com.voxeo.moho.common.event.MohoJoinCompleteEvent; import com.voxeo.moho.common.event.MohoMediaResourceDisconnectEvent; import com.voxeo.moho.common.event.MohoUnjoinCompleteEvent; import com.voxeo.moho.event.JoinCompleteEvent; import com.voxeo.moho.event.JoinCompleteEvent.Cause; import com.voxeo.moho.event.UnjoinCompleteEvent; import com.voxeo.moho.remotejoin.RemoteParticipant; import com.voxeo.moho.sip.JoinDelegate; import com.voxeo.moho.spi.ExecutionContext; import com.voxeo.moho.util.IDGenerator; public class VoiceXMLDialogImpl extends DispatchableEventSource implements Dialog, ParticipantContainer { private static final Logger LOG = Logger.getLogger(VoiceXMLDialogImpl.class); protected enum DialogState { IDLE, PREPARING, PREPARED, STARTED }; protected DialogState _state = DialogState.IDLE; protected VoiceXMLEndpoint _address; protected Parameters _options; protected Map<java.lang.String, java.lang.Object> _sessionVariables; protected MediaSession _media; protected VxmlDialog _dialog; protected FutureTask<Map<String, Object>> _future; protected Map<String, Object> _result; protected Object _lock = new Object(); protected JoineeData _joinees = new JoineeData(); protected VoiceXMLDialogImpl(final ExecutionContext ctx, final VoiceXMLEndpoint address, final Map<Object, Object> params) { super(ctx); _id = IDGenerator.generateId(_context, RemoteParticipant.RemoteParticipant_TYPE_DIALOG); try { _media = ctx.getMSFactory().createMediaSession(); _dialog = _media.createVxmlDialog(null); _options = _media.createParameters(); _sessionVariables = new HashMap<String, Object>(); _dialog.addListener(new VxmlListener()); _address = address; if (params != null) { for (final Object key : params.keySet()) { if (key instanceof Parameter) { _options.put((Parameter) key, params.get(key)); } else { _sessionVariables.put(key.toString(), params.get(key)); } } } ((ApplicationContextImpl) _context).addParticipant(this); } catch (final MsControlException e) { throw new MediaException(e); } } @Override public int hashCode() { return _dialog.hashCode(); } @Override public boolean equals(final Object o) { if (!(o instanceof VoiceXMLDialogImpl)) { return false; } if (this == o) { return true; } return _dialog.equals(((VoiceXMLDialogImpl) o).getMediaObject()); } @Override public String toString() { return new StringBuilder().append(this.getClass().getSimpleName()).append("[").append(_state).append("]") .toString(); } @Override public Endpoint getAddress() { return _address; } @Override public MediaObject getMediaObject() { return _dialog; } @Override public void disconnect() { ((ApplicationContextImpl) _context).removeParticipant(getId()); try { _dialog.terminate(true); } catch (final Exception e) { LOG.warn("Exception when terminate VxmlDialog", e); } try { _dialog.release(); } catch (final Exception e) { LOG.warn("Exception when release VxmlDialog", e); } try { _media.release(); } catch (final Exception e) { LOG.warn("Exception when release MediaSession", e); } _media = null; Participant[] _joineesArray = _joinees.getJoinees(); for (Participant participant : _joineesArray) { if (participant instanceof ParticipantContainer) { try { ((ParticipantContainer) participant).doUnjoin(this, false); } catch (Exception e) { LOG.error("", e); } MohoUnjoinCompleteEvent event = new MohoUnjoinCompleteEvent(participant, VoiceXMLDialogImpl.this, UnjoinCompleteEvent.Cause.DISCONNECT, false); participant.dispatch(event); dispatch(new MohoUnjoinCompleteEvent(this, participant, UnjoinCompleteEvent.Cause.DISCONNECT, true)); } } _joinees.clear(); joinDelegates.clear(); this.dispatch(new MohoMediaResourceDisconnectEvent<Dialog>(this)); } @Override public Participant[] getParticipants() { return _joinees.getJoinees(); } @Override public Participant[] getParticipants(final Direction direction) { return _joinees.getJoinees(direction); } @Override public void addParticipant(final Participant p, final JoinType type, final Direction direction, Participant realJoined) { _joinees.add(p, type, direction, realJoined); } @Override public Joint join(final Participant other, final JoinType type, final Direction direction) throws IllegalStateException { return this.join(other, type, false, direction); } @Override public Joint join(Participant other, JoinType type, boolean force, Direction direction, boolean dtmfPassThough) { //TODO can specify the dtmfPassThough for VxmlDialog? return this.join(other, type, force, direction); } @Override public Joint join(final Participant other, final JoinType type, final boolean force, final Direction direction) throws IllegalStateException { synchronized (_lock) { if (_state != DialogState.IDLE) { throw new IllegalStateException("Cannot join when the dialog is starting."); } if (_joinees.contains(other)) { return new JointImpl(_context.getExecutor(), new JointImpl.DummyJoinWorker(VoiceXMLDialogImpl.this, other)); } } if (other instanceof Call) { return other.join(this, type, direction); } else if (other instanceof RemoteParticipant) { return other.join(this, type, direction); } else { if (!(other.getMediaObject() instanceof Joinable)) { throw new IllegalArgumentException("MediaObject isn't joinable."); } return new JointImpl(_context.getExecutor(), new JoinWorker() { @Override public JoinCompleteEvent call() throws Exception { JoinCompleteEvent event = null; try { synchronized (_lock) { _dialog.join(direction, (Joinable) other.getMediaObject()); _joinees.add(other, type, direction); ((ParticipantContainer) other).addParticipant(VoiceXMLDialogImpl.this, type, direction, null); event = new MohoJoinCompleteEvent(VoiceXMLDialogImpl.this, other, Cause.JOINED, true); } } catch (final Exception e) { event = new MohoJoinCompleteEvent(VoiceXMLDialogImpl.this, other, Cause.ERROR, e, true); throw new MediaException(e); } finally { VoiceXMLDialogImpl.this.dispatch(event); MohoJoinCompleteEvent event2 = new MohoJoinCompleteEvent(other, VoiceXMLDialogImpl.this, event.getCause(), false); other.dispatch(event2); } return event; } @Override public boolean cancel() { return false; } }); } } public MohoUnjoinCompleteEvent doUnjoin(final Participant p, boolean callPeerUnjoin) throws Exception { MohoUnjoinCompleteEvent event = null; synchronized (_lock) { if (!_joinees.contains(p)) { event = new MohoUnjoinCompleteEvent(VoiceXMLDialogImpl.this, p, UnjoinCompleteEvent.Cause.NOT_JOINED, true); VoiceXMLDialogImpl.this.dispatch(event); return event; } try { _joinees.remove(p); if (p.getMediaObject() instanceof Joinable) { _dialog.unjoin((Joinable) p.getMediaObject()); } if (callPeerUnjoin) { ((ParticipantContainer) p).doUnjoin(this, false); } event = new MohoUnjoinCompleteEvent(VoiceXMLDialogImpl.this, p, UnjoinCompleteEvent.Cause.SUCCESS_UNJOIN, true); } catch (final Exception e) { LOG.error("", e); event = new MohoUnjoinCompleteEvent(VoiceXMLDialogImpl.this, p, UnjoinCompleteEvent.Cause.FAIL_UNJOIN, e, true); throw e; } finally { if (event == null) { event = new MohoUnjoinCompleteEvent(VoiceXMLDialogImpl.this, p, UnjoinCompleteEvent.Cause.FAIL_UNJOIN, true); } VoiceXMLDialogImpl.this.dispatch(event); } } return event; } @Override public Unjoint unjoin(final Participant other) { Unjoint task = new UnjointImpl(_context.getExecutor(), new Callable<UnjoinCompleteEvent>() { @Override public UnjoinCompleteEvent call() throws Exception { return doUnjoin(other, true); } }); return task; } public void prepare() { synchronized (_lock) { if (_state != DialogState.IDLE) { throw new IllegalStateException("" + this); } _dialog.prepare(_address.getDocumentURL(), _options, _sessionVariables); setState(DialogState.PREPARING); } } public void start() { synchronized (_lock) { if (_state != DialogState.PREPARING && _state != DialogState.PREPARED) { throw new IllegalStateException("" + this); } while (_state != DialogState.PREPARED && _state != DialogState.IDLE) { try { _lock.wait(); } catch (final InterruptedException e) { // ignore } } if (_state == DialogState.IDLE) { throw new IllegalStateException("Error: " + this); } _future = new FutureTask<Map<String, Object>>(new Callable<Map<String, Object>>() { @Override public Map<String, Object> call() throws Exception { synchronized (_lock) { Map<String, Object> retval = _result = null; if (_state == DialogState.PREPARED) { _dialog.start(_sessionVariables); while (_result == null && _state != DialogState.IDLE) { _lock.wait(); } } retval = _result; return retval; } } }); new Thread(_future).start(); } } public void terminate(final boolean immediate) { synchronized (_lock) { _dialog.terminate(immediate); while (_state != DialogState.IDLE) { try { _lock.wait(); } catch (final InterruptedException e) { // ignore } } } } protected void setState(final DialogState state) { synchronized (_lock) { _state = state; } } protected class VxmlListener implements MediaEventListener<VxmlDialogEvent> { public void onEvent(final VxmlDialogEvent event) { if (event.getEventType().equals(VxmlDialogEvent.PREPARED)) { synchronized (_lock) { if (_state == DialogState.PREPARING) { setState(DialogState.PREPARED); _lock.notifyAll(); } } } else if (event.getEventType().equals(VxmlDialogEvent.STARTED)) { synchronized (_lock) { setState(DialogState.STARTED); } } else if (event.getEventType().equals(VxmlDialogEvent.EXITED)) { synchronized (_lock) { _result = event.getNameList(); _lock.notifyAll(); setState(DialogState.IDLE); } } else if (event.getEventType().equals(VxmlDialogEvent.DISCONNECTION_REQUESTED) || event.getEventType().equals(VxmlDialogEvent.ERROR_EVENT)) { synchronized (_lock) { setState(DialogState.IDLE); _lock.notifyAll(); } } } } @Override public boolean cancel(final boolean mayInterruptIfRunning) { return _future.cancel(mayInterruptIfRunning); } @Override public Map<String, Object> get() throws InterruptedException, ExecutionException { return _future.get(); } @Override public Map<String, Object> get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return _future.get(timeout, unit); } @Override public boolean isCancelled() { return _future.isCancelled(); } @Override public boolean isDone() { return _future.isDone(); } @Override public String getRemoteAddress() { return _id; } private Map<String, JoinDelegate> joinDelegates = new ConcurrentHashMap<String, JoinDelegate>(); @Override public void startJoin(Participant participant, JoinDelegate delegate) { joinDelegates.put(participant.getId(), delegate); } @Override public void joinDone(Participant participant, JoinDelegate delegate) { joinDelegates.remove(participant.getId()); } public JoinDelegate getJoinDelegate(String id) { return joinDelegates.get(id); } @Override public Direction getDirection(Participant participant) { return _joinees.getDirection(participant); } @Override public byte[] getJoinSDP() { throw new UnsupportedOperationException(""); } @Override public void processSDPAnswer(byte[] sdp) throws IOException { throw new UnsupportedOperationException(""); } @Override public byte[] processSDPOffer(byte[] sdp) throws IOException { throw new UnsupportedOperationException(""); } @Override public JoinType getJoinType(Participant participant) { return _joinees.getJoinType(participant); } @Override public Unjoint unjoin(final Participant other, final boolean callPeerUnjoin) throws Exception { Unjoint task = new UnjointImpl(_context.getExecutor(), new Callable<UnjoinCompleteEvent>() { @Override public UnjoinCompleteEvent call() throws Exception { return doUnjoin(other, callPeerUnjoin); } }); return task; } }