View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.model;
17  
18  import java.beans.PropertyChangeEvent;
19  import java.beans.PropertyChangeListener;
20  import java.beans.PropertyChangeSupport;
21  import java.lang.reflect.Method;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.concurrent.TimeUnit;
26  import java.util.concurrent.locks.ReentrantLock;
27  import java.util.logging.Level;
28  import java.util.logging.Logger;
29  
30  import org.fourthline.cling.model.meta.LocalService;
31  import org.fourthline.cling.model.meta.StateVariable;
32  import org.fourthline.cling.model.state.StateVariableAccessor;
33  import org.fourthline.cling.model.state.StateVariableValue;
34  import org.seamless.util.Exceptions;
35  import org.seamless.util.Reflections;
36  
37  /**
38   * Default implementation, creates and manages a single instance of a plain Java bean.
39   * <p>
40   * Creates instance of the defined service class when it is first needed (acts as a factory),
41   * manages the instance in a field (it's shared), and synchronizes (locks) all
42   * multi-threaded access. A locking attempt will timeout after 500 milliseconds with
43   * a runtime exception if another operation is already in progress. Override
44   * {@link #getLockTimeoutMillis()} to customize this behavior, e.g. if your service
45   * bean is slow and requires more time for typical action executions or state
46   * variable reading.
47   * </p>
48   *
49   * @author Christian Bauer
50   */
51  public class DefaultServiceManager<T> implements ServiceManager<T> {
52  
53      private static Logger log = Logger.getLogger(DefaultServiceManager.class.getName());
54  
55      final protected LocalService<T> service;
56      final protected Class<T> serviceClass;
57      final protected ReentrantLock lock = new ReentrantLock(true);
58  
59      // Locking!
60      protected T serviceImpl;
61      protected PropertyChangeSupport propertyChangeSupport;
62  
63      protected DefaultServiceManager(LocalService<T> service) {
64          this(service, null);
65      }
66  
67      public DefaultServiceManager(LocalService<T> service, Class<T> serviceClass) {
68          this.service = service;
69          this.serviceClass = serviceClass;
70      }
71  
72      // The monitor entry and exit methods
73  
74      protected void lock() {
75          try {
76              if (lock.tryLock(getLockTimeoutMillis(), TimeUnit.MILLISECONDS)) {
77                  if (log.isLoggable(Level.FINEST))
78                      log.finest("Acquired lock");
79              } else {
80                  throw new RuntimeException("Failed to acquire lock in milliseconds: " + getLockTimeoutMillis());
81              }
82          } catch (InterruptedException e) {
83              throw new RuntimeException("Failed to acquire lock:" + e);
84          }
85      }
86  
87      protected void unlock() {
88          if (log.isLoggable(Level.FINEST))
89              log.finest("Releasing lock");
90          lock.unlock();
91      }
92  
93      protected int getLockTimeoutMillis() {
94          return 500;
95      }
96  
97      public LocalService<T> getService() {
98          return service;
99      }
100 
101     public T getImplementation() {
102         lock();
103         try {
104             if (serviceImpl == null) {
105                 init();
106             }
107             return serviceImpl;
108         } finally {
109             unlock();
110         }
111     }
112 
113     public PropertyChangeSupport getPropertyChangeSupport() {
114         lock();
115         try {
116             if (propertyChangeSupport == null) {
117                 init();
118             }
119             return propertyChangeSupport;
120         } finally {
121             unlock();
122         }
123     }
124 
125     public void execute(Command<T> cmd) throws Exception {
126         lock();
127         try {
128             cmd.execute(this);
129         } finally {
130             unlock();
131         }
132     }
133 
134     @Override
135     public Collection<StateVariableValue> getCurrentState() throws Exception {
136         lock();
137         try {
138             Collection<StateVariableValue> values = readInitialEventedStateVariableValues();
139             if (values != null) {
140                 log.fine("Obtained initial state variable values for event, skipping individual state variable accessors");
141                 return values;
142             }
143             values = new ArrayList<>();
144             for (StateVariable stateVariable : getService().getStateVariables()) {
145                 if (stateVariable.getEventDetails().isSendEvents()) {
146                     StateVariableAccessor accessor = getService().getAccessor(stateVariable);
147                     if (accessor == null)
148                         throw new IllegalStateException("No accessor for evented state variable");
149                     values.add(accessor.read(stateVariable, getImplementation()));
150                 }
151             }
152             return values;
153         } finally {
154             unlock();
155         }
156     }
157 
158     protected Collection<StateVariableValue> getCurrentState(String[] variableNames) throws Exception {
159         lock();
160         try {
161             Collection<StateVariableValue> values = new ArrayList<>();
162             for (String variableName : variableNames) {
163                 variableName = variableName.trim();
164 
165                 StateVariable stateVariable = getService().getStateVariable(variableName);
166                 if (stateVariable == null || !stateVariable.getEventDetails().isSendEvents()) {
167                     log.fine("Ignoring unknown or non-evented state variable: " + variableName);
168                     continue;
169                 }
170 
171                 StateVariableAccessor accessor = getService().getAccessor(stateVariable);
172                 if (accessor == null) {
173                     log.warning("Ignoring evented state variable without accessor: " + variableName);
174                     continue;
175                 }
176                 values.add(accessor.read(stateVariable, getImplementation()));
177             }
178             return values;
179         } finally {
180             unlock();
181         }
182     }
183 
184     protected void init() {
185         log.fine("No service implementation instance available, initializing...");
186         try {
187             // The actual instance we ware going to use and hold a reference to (1:1 instance for manager)
188             serviceImpl = createServiceInstance();
189 
190             // How the implementation instance will tell us about property changes
191             propertyChangeSupport = createPropertyChangeSupport(serviceImpl);
192             propertyChangeSupport.addPropertyChangeListener(createPropertyChangeListener(serviceImpl));
193 
194         } catch (Exception ex) {
195             throw new RuntimeException("Could not initialize implementation: " + ex, ex);
196         }
197     }
198 
199     protected T createServiceInstance() throws Exception {
200         if (serviceClass == null) {
201             throw new IllegalStateException("Subclass has to provide service class or override createServiceInstance()");
202         }
203         try {
204             // Use this constructor if possible
205             return serviceClass.getConstructor(LocalService.class).newInstance(getService());
206         } catch (NoSuchMethodException ex) {
207             log.fine("Creating new service implementation instance with no-arg constructor: " + serviceClass.getName());
208             return serviceClass.newInstance();
209         }
210     }
211 
212     protected PropertyChangeSupport createPropertyChangeSupport(T serviceImpl) throws Exception {
213         Method m;
214         if ((m = Reflections.getGetterMethod(serviceImpl.getClass(), "propertyChangeSupport")) != null &&
215             PropertyChangeSupport.class.isAssignableFrom(m.getReturnType())) {
216             log.fine("Service implementation instance offers PropertyChangeSupport, using that: " + serviceImpl.getClass().getName());
217             return (PropertyChangeSupport) m.invoke(serviceImpl);
218         }
219         log.fine("Creating new PropertyChangeSupport for service implementation: " + serviceImpl.getClass().getName());
220         return new PropertyChangeSupport(serviceImpl);
221     }
222 
223     protected PropertyChangeListener createPropertyChangeListener(T serviceImpl) throws Exception {
224         return new DefaultPropertyChangeListener();
225     }
226 
227     protected Collection<StateVariableValue> readInitialEventedStateVariableValues() throws Exception {
228         return null;
229     }
230 
231     @Override
232     public String toString() {
233         return "(" + getClass().getSimpleName() + ") Implementation: " + serviceImpl;
234     }
235 
236     protected class DefaultPropertyChangeListener implements PropertyChangeListener {
237 
238         public void propertyChange(PropertyChangeEvent e) {
239             log.finer("Property change event on local service: " + e.getPropertyName());
240 
241             // Prevent recursion
242             if (e.getPropertyName().equals(EVENTED_STATE_VARIABLES)) return;
243 
244             String[] variableNames = ModelUtil.fromCommaSeparatedList(e.getPropertyName());
245             log.fine("Changed variable names: " + Arrays.toString(variableNames));
246 
247             try {
248                 Collection<StateVariableValue> currentValues = getCurrentState(variableNames);
249 
250                 if (!currentValues.isEmpty()) {
251                     getPropertyChangeSupport().firePropertyChange(
252                         EVENTED_STATE_VARIABLES,
253                         null,
254                         currentValues
255                     );
256                 }
257 
258             } catch (Exception ex) {
259                 // TODO: Is it OK to only log this error? It means we keep running although we couldn't send events?
260                 log.log(
261                     Level.SEVERE,
262                     "Error reading state of service after state variable update event: " + Exceptions.unwrap(ex),
263                     ex
264                 );
265             }
266         }
267     }
268 }