/* * Copyright (C) 2010 Teleal GmbH, Switzerland * * This program 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 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.teleal.cling.support.avtransport.impl; import org.teleal.cling.model.ModelUtil; import org.teleal.cling.model.types.ErrorCode; import org.teleal.cling.model.types.UnsignedIntegerFourBytes; import org.teleal.cling.support.avtransport.AVTransportErrorCode; import org.teleal.cling.support.avtransport.AVTransportException; import org.teleal.cling.support.avtransport.AbstractAVTransportService; import org.teleal.cling.support.avtransport.impl.state.AbstractState; import org.teleal.cling.support.lastchange.LastChange; import org.teleal.cling.support.model.AVTransport; import org.teleal.cling.support.model.DeviceCapabilities; import org.teleal.cling.support.model.MediaInfo; import org.teleal.cling.support.model.PlayMode; import org.teleal.cling.support.model.PositionInfo; import org.teleal.cling.support.model.RecordQualityMode; import org.teleal.cling.support.model.SeekMode; import org.teleal.cling.support.model.StorageMedium; import org.teleal.cling.support.model.TransportAction; import org.teleal.cling.support.model.TransportInfo; import org.teleal.cling.support.model.TransportSettings; import org.teleal.common.statemachine.StateMachineBuilder; import org.teleal.common.statemachine.TransitionException; import java.net.URI; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; /** * State-machine based implementation of AVTransport service. * <p> * One logical AVTransport is represented by: * </p> * <ul> * <li> * One {@link org.teleal.cling.support.avtransport.impl.AVTransportStateMachine} * instance that accepts the action method call as a proxy. * </li> * <li> * Each state machine holds several instances of * {@link org.teleal.cling.support.avtransport.impl.state.AbstractState}, created on * instantation of the state machine. The "current" state will be the target of * the action call. It is the state implementation that decides how to handle the * call and what the next state is after a possible transition. * </li> * <li> * Each state has a reference to an implementation of * {@link org.teleal.cling.support.model.AVTransport}, where the state can hold * information about well, the state. * </li> * </ul> * <p> * Simplified, this means that each AVTransport instance ID is typically handled by * one state machine, and the internal state of that machine is stored in an * <code>AVTransport</code>. * </p> * <p> * Override the {@link #createTransport(org.teleal.cling.model.types.UnsignedIntegerFourBytes, org.teleal.cling.support.lastchange.LastChange)} * method to utilize a subclass of <code>AVTransport</code> as your internal state holder. * </p> * * @author Christian Bauer */ public class AVTransportService<T extends AVTransport> extends AbstractAVTransportService { final private static Logger log = Logger.getLogger(AVTransportService.class.getName()); final private Map<Long, AVTransportStateMachine> stateMachines = new ConcurrentHashMap(); final Class<? extends AVTransportStateMachine> stateMachineDefinition; final Class<? extends AbstractState> initialState; final Class<? extends AVTransport> transportClass; public AVTransportService(Class<? extends AVTransportStateMachine> stateMachineDefinition, Class<? extends AbstractState> initialState) { this(stateMachineDefinition, initialState, (Class<T>)AVTransport.class); } public AVTransportService(Class<? extends AVTransportStateMachine> stateMachineDefinition, Class<? extends AbstractState> initialState, Class<T> transportClass) { this.stateMachineDefinition = stateMachineDefinition; this.initialState = initialState; this.transportClass = transportClass; } public void setAVTransportURI(UnsignedIntegerFourBytes instanceId, String currentURI, String currentURIMetaData) throws AVTransportException { URI uri; try { uri = new URI(currentURI); } catch (Exception ex) { throw new AVTransportException( ErrorCode.INVALID_ARGS, "CurrentURI can not be null or malformed" ); } try { AVTransportStateMachine transportStateMachine = findStateMachine(instanceId, true); transportStateMachine.setTransportURI(uri, currentURIMetaData); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void setNextAVTransportURI(UnsignedIntegerFourBytes instanceId, String nextURI, String nextURIMetaData) throws AVTransportException { URI uri; try { uri = new URI(nextURI); } catch (Exception ex) { throw new AVTransportException( ErrorCode.INVALID_ARGS, "NextURI can not be null or malformed" ); } try { AVTransportStateMachine transportStateMachine = findStateMachine(instanceId, true); transportStateMachine.setNextTransportURI(uri, nextURIMetaData); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void setPlayMode(UnsignedIntegerFourBytes instanceId, String newPlayMode) throws AVTransportException { AVTransport transport = findStateMachine(instanceId).getCurrentState().getTransport(); try { transport.setTransportSettings( new TransportSettings( PlayMode.valueOf(newPlayMode), transport.getTransportSettings().getRecQualityMode() ) ); } catch (IllegalArgumentException ex) { throw new AVTransportException( AVTransportErrorCode.PLAYMODE_NOT_SUPPORTED, "Unsupported play mode: " + newPlayMode ); } } public void setRecordQualityMode(UnsignedIntegerFourBytes instanceId, String newRecordQualityMode) throws AVTransportException { AVTransport transport = findStateMachine(instanceId).getCurrentState().getTransport(); try { transport.setTransportSettings( new TransportSettings( transport.getTransportSettings().getPlayMode(), RecordQualityMode.valueOrExceptionOf(newRecordQualityMode) ) ); } catch (IllegalArgumentException ex) { throw new AVTransportException( AVTransportErrorCode.RECORDQUALITYMODE_NOT_SUPPORTED, "Unsupported record quality mode: " + newRecordQualityMode ); } } public MediaInfo getMediaInfo(UnsignedIntegerFourBytes instanceId) throws AVTransportException { return findStateMachine(instanceId).getCurrentState().getTransport().getMediaInfo(); } public TransportInfo getTransportInfo(UnsignedIntegerFourBytes instanceId) throws AVTransportException { return findStateMachine(instanceId).getCurrentState().getTransport().getTransportInfo(); } public PositionInfo getPositionInfo(UnsignedIntegerFourBytes instanceId) throws AVTransportException { return findStateMachine(instanceId).getCurrentState().getTransport().getPositionInfo(); } public DeviceCapabilities getDeviceCapabilities(UnsignedIntegerFourBytes instanceId) throws AVTransportException { return findStateMachine(instanceId).getCurrentState().getTransport().getDeviceCapabilities(); } public TransportSettings getTransportSettings(UnsignedIntegerFourBytes instanceId) throws AVTransportException { return findStateMachine(instanceId).getCurrentState().getTransport().getTransportSettings(); } public String getCurrentTransportActions(UnsignedIntegerFourBytes instanceId) throws AVTransportException { AVTransportStateMachine stateMachine = findStateMachine(instanceId); try { TransportAction[] actions = stateMachine.getCurrentState().getCurrentTransportActions(); return ModelUtil.toCommaSeparatedList(actions); } catch (TransitionException ex) { return ""; // TODO: Empty string is not defined in spec but seems reasonable for no available action? } } public void stop(UnsignedIntegerFourBytes instanceId) throws AVTransportException { try { findStateMachine(instanceId).stop(); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void play(UnsignedIntegerFourBytes instanceId, String speed) throws AVTransportException { try { findStateMachine(instanceId).play(speed); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void pause(UnsignedIntegerFourBytes instanceId) throws AVTransportException { try { findStateMachine(instanceId).pause(); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void record(UnsignedIntegerFourBytes instanceId) throws AVTransportException { try { findStateMachine(instanceId).record(); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void seek(UnsignedIntegerFourBytes instanceId, String unit, String target) throws AVTransportException { SeekMode seekMode; try { seekMode = SeekMode.valueOrExceptionOf(unit); } catch (IllegalArgumentException ex) { throw new AVTransportException( AVTransportErrorCode.SEEKMODE_NOT_SUPPORTED, "Unsupported seek mode: " + unit ); } try { findStateMachine(instanceId).seek(seekMode, target); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void next(UnsignedIntegerFourBytes instanceId) throws AVTransportException { try { findStateMachine(instanceId).next(); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } public void previous(UnsignedIntegerFourBytes instanceId) throws AVTransportException { try { findStateMachine(instanceId).previous(); } catch (TransitionException ex) { throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } } protected AVTransportStateMachine findStateMachine(UnsignedIntegerFourBytes instanceId) throws AVTransportException { return findStateMachine(instanceId, true); } protected AVTransportStateMachine findStateMachine(UnsignedIntegerFourBytes instanceId, boolean createDefaultTransport) throws AVTransportException { synchronized (stateMachines) { long id = instanceId.getValue(); AVTransportStateMachine stateMachine = stateMachines.get(id); if (stateMachine == null && id == 0 && createDefaultTransport) { log.fine("Creating default transport instance with ID '0'"); stateMachine = createStateMachine(instanceId); stateMachines.put(id, stateMachine); } else if (stateMachine == null) { throw new AVTransportException(AVTransportErrorCode.INVALID_INSTANCE_ID); } log.fine("Found transport control with ID '" + id + "'"); return stateMachine; } } protected AVTransportStateMachine createStateMachine(UnsignedIntegerFourBytes instanceId) { // Create a proxy that delegates all calls to the right state implementation, working on the T state return StateMachineBuilder.build( stateMachineDefinition, initialState, new Class[]{transportClass}, new Object[]{createTransport(instanceId, getLastChange())} ); } protected AVTransport createTransport(UnsignedIntegerFourBytes instanceId, LastChange lastChange) { return new AVTransport(instanceId, lastChange, StorageMedium.NETWORK); } }